2020-08-03 11:48:33 +00:00
|
|
|
package layer
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2021-05-28 20:48:23 +00:00
|
|
|
"crypto/ecdsa"
|
2022-05-18 07:48:30 +00:00
|
|
|
"crypto/rand"
|
2021-05-20 10:14:17 +00:00
|
|
|
"fmt"
|
2020-08-03 11:48:33 +00:00
|
|
|
"io"
|
2020-10-24 13:09:22 +00:00
|
|
|
"net/url"
|
2022-08-01 16:52:09 +00:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
2022-05-20 15:02:00 +00:00
|
|
|
"time"
|
2020-08-03 11:48:33 +00:00
|
|
|
|
2022-12-14 16:58:00 +00:00
|
|
|
"github.com/TrueCloudLab/frostfs-s3-gw/api"
|
|
|
|
"github.com/TrueCloudLab/frostfs-s3-gw/api/data"
|
|
|
|
"github.com/TrueCloudLab/frostfs-s3-gw/api/errors"
|
|
|
|
"github.com/TrueCloudLab/frostfs-s3-gw/api/layer/encryption"
|
|
|
|
"github.com/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
|
|
|
|
"github.com/TrueCloudLab/frostfs-sdk-go/bearer"
|
|
|
|
cid "github.com/TrueCloudLab/frostfs-sdk-go/container/id"
|
|
|
|
"github.com/TrueCloudLab/frostfs-sdk-go/eacl"
|
|
|
|
"github.com/TrueCloudLab/frostfs-sdk-go/netmap"
|
|
|
|
oid "github.com/TrueCloudLab/frostfs-sdk-go/object/id"
|
|
|
|
"github.com/TrueCloudLab/frostfs-sdk-go/session"
|
|
|
|
"github.com/TrueCloudLab/frostfs-sdk-go/user"
|
2022-03-05 06:58:54 +00:00
|
|
|
"github.com/nats-io/nats.go"
|
2021-10-19 15:08:07 +00:00
|
|
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
2020-08-03 11:48:33 +00:00
|
|
|
"go.uber.org/zap"
|
|
|
|
)
|
|
|
|
|
|
|
|
type (
|
2022-04-29 13:08:22 +00:00
|
|
|
EventListener interface {
|
2022-03-05 06:58:54 +00:00
|
|
|
Subscribe(context.Context, string, MsgHandler) error
|
|
|
|
Listen(context.Context)
|
|
|
|
}
|
|
|
|
|
|
|
|
MsgHandler interface {
|
|
|
|
HandleMessage(context.Context, *nats.Msg) error
|
|
|
|
}
|
|
|
|
|
|
|
|
MsgHandlerFunc func(context.Context, *nats.Msg) error
|
|
|
|
|
2022-09-12 13:46:55 +00:00
|
|
|
BucketResolver interface {
|
|
|
|
Resolve(ctx context.Context, name string) (cid.ID, error)
|
|
|
|
}
|
|
|
|
|
2020-08-03 11:48:33 +00:00
|
|
|
layer struct {
|
2022-12-20 08:38:58 +00:00
|
|
|
frostFS FrostFS
|
2021-08-18 13:48:58 +00:00
|
|
|
log *zap.Logger
|
2021-10-19 15:08:07 +00:00
|
|
|
anonKey AnonymousKey
|
2022-09-12 13:46:55 +00:00
|
|
|
resolver BucketResolver
|
2022-04-29 13:08:22 +00:00
|
|
|
ncontroller EventListener
|
2022-10-03 14:33:49 +00:00
|
|
|
cache *Cache
|
2022-04-22 07:18:21 +00:00
|
|
|
treeService TreeService
|
2020-10-19 01:04:37 +00:00
|
|
|
}
|
|
|
|
|
2021-11-22 09:16:05 +00:00
|
|
|
Config struct {
|
2022-03-05 06:58:54 +00:00
|
|
|
ChainAddress string
|
|
|
|
Caches *CachesConfig
|
|
|
|
AnonKey AnonymousKey
|
2022-09-12 13:46:55 +00:00
|
|
|
Resolver BucketResolver
|
2022-04-22 07:18:21 +00:00
|
|
|
TreeService TreeService
|
2021-11-22 09:16:05 +00:00
|
|
|
}
|
|
|
|
|
2021-10-19 15:08:07 +00:00
|
|
|
// AnonymousKey contains data for anonymous requests.
|
|
|
|
AnonymousKey struct {
|
|
|
|
Key *keys.PrivateKey
|
|
|
|
}
|
|
|
|
|
2021-05-13 20:25:31 +00:00
|
|
|
// GetObjectParams stores object get request parameters.
|
2020-08-03 11:48:33 +00:00
|
|
|
GetObjectParams struct {
|
2021-08-10 10:03:09 +00:00
|
|
|
Range *RangeParams
|
2021-09-10 06:56:56 +00:00
|
|
|
ObjectInfo *data.ObjectInfo
|
2022-06-02 16:56:04 +00:00
|
|
|
BucketInfo *data.BucketInfo
|
2021-08-19 06:55:22 +00:00
|
|
|
Writer io.Writer
|
2022-08-11 08:48:58 +00:00
|
|
|
Encryption encryption.Params
|
2021-08-10 10:03:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// HeadObjectParams stores object head request parameters.
|
|
|
|
HeadObjectParams struct {
|
2022-03-18 13:04:09 +00:00
|
|
|
BktInfo *data.BucketInfo
|
2021-08-10 10:03:09 +00:00
|
|
|
Object string
|
|
|
|
VersionID string
|
2020-08-03 11:48:33 +00:00
|
|
|
}
|
|
|
|
|
2022-05-26 13:11:14 +00:00
|
|
|
// ObjectVersion stores object version info.
|
|
|
|
ObjectVersion struct {
|
2022-06-15 12:17:29 +00:00
|
|
|
BktInfo *data.BucketInfo
|
|
|
|
ObjectName string
|
|
|
|
VersionID string
|
|
|
|
NoErrorOnDeleteMarker bool
|
2022-05-26 13:11:14 +00:00
|
|
|
}
|
|
|
|
|
2021-06-24 11:10:00 +00:00
|
|
|
// RangeParams stores range header request parameters.
|
|
|
|
RangeParams struct {
|
|
|
|
Start uint64
|
|
|
|
End uint64
|
|
|
|
}
|
|
|
|
|
2021-05-13 20:25:31 +00:00
|
|
|
// PutObjectParams stores object put request parameters.
|
2020-08-03 11:48:33 +00:00
|
|
|
PutObjectParams struct {
|
2022-08-11 22:48:56 +00:00
|
|
|
BktInfo *data.BucketInfo
|
|
|
|
Object string
|
|
|
|
Size int64
|
|
|
|
Reader io.Reader
|
|
|
|
Header map[string]string
|
|
|
|
Lock *data.ObjectLock
|
|
|
|
Encryption encryption.Params
|
|
|
|
CopiesNumber uint32
|
2020-08-03 11:48:33 +00:00
|
|
|
}
|
|
|
|
|
2022-03-31 06:24:29 +00:00
|
|
|
DeleteObjectParams struct {
|
2022-06-24 12:39:30 +00:00
|
|
|
BktInfo *data.BucketInfo
|
|
|
|
Objects []*VersionedObject
|
|
|
|
Settings *data.BucketSettings
|
2022-03-31 06:24:29 +00:00
|
|
|
}
|
|
|
|
|
2022-02-28 08:02:05 +00:00
|
|
|
// PutSettingsParams stores object copy request parameters.
|
|
|
|
PutSettingsParams struct {
|
|
|
|
BktInfo *data.BucketInfo
|
|
|
|
Settings *data.BucketSettings
|
2021-08-10 08:58:40 +00:00
|
|
|
}
|
|
|
|
|
2021-10-04 14:32:35 +00:00
|
|
|
// PutCORSParams stores PutCORS request parameters.
|
|
|
|
PutCORSParams struct {
|
2022-08-11 22:48:56 +00:00
|
|
|
BktInfo *data.BucketInfo
|
|
|
|
Reader io.Reader
|
|
|
|
CopiesNumber uint32
|
2021-10-04 14:32:35 +00:00
|
|
|
}
|
|
|
|
|
2021-05-13 20:25:31 +00:00
|
|
|
// CopyObjectParams stores object copy request parameters.
|
2020-08-03 11:48:33 +00:00
|
|
|
CopyObjectParams struct {
|
2022-08-17 11:18:36 +00:00
|
|
|
SrcObject *data.ObjectInfo
|
|
|
|
ScrBktInfo *data.BucketInfo
|
|
|
|
DstBktInfo *data.BucketInfo
|
|
|
|
DstObject string
|
|
|
|
SrcSize int64
|
|
|
|
Header map[string]string
|
|
|
|
Range *RangeParams
|
|
|
|
Lock *data.ObjectLock
|
|
|
|
Encryption encryption.Params
|
|
|
|
CopiesNuber uint32
|
2020-08-03 11:48:33 +00:00
|
|
|
}
|
2021-06-23 20:21:15 +00:00
|
|
|
// CreateBucketParams stores bucket create request parameters.
|
|
|
|
CreateBucketParams struct {
|
2022-06-21 15:21:20 +00:00
|
|
|
Name string
|
|
|
|
Policy netmap.PlacementPolicy
|
|
|
|
EACL *eacl.Table
|
|
|
|
SessionContainerCreation *session.Container
|
|
|
|
SessionEACL *session.Container
|
|
|
|
LocationConstraint string
|
|
|
|
ObjectLockEnabled bool
|
2021-06-23 20:21:15 +00:00
|
|
|
}
|
2021-07-21 11:59:46 +00:00
|
|
|
// PutBucketACLParams stores put bucket acl request parameters.
|
|
|
|
PutBucketACLParams struct {
|
2022-06-21 15:21:20 +00:00
|
|
|
BktInfo *data.BucketInfo
|
|
|
|
EACL *eacl.Table
|
|
|
|
SessionToken *session.Container
|
2021-07-21 11:59:46 +00:00
|
|
|
}
|
2021-06-23 20:25:00 +00:00
|
|
|
// DeleteBucketParams stores delete bucket request parameters.
|
|
|
|
DeleteBucketParams struct {
|
2022-06-22 13:22:28 +00:00
|
|
|
BktInfo *data.BucketInfo
|
|
|
|
SessionToken *session.Container
|
2021-06-23 20:25:00 +00:00
|
|
|
}
|
2021-10-04 14:30:38 +00:00
|
|
|
|
2021-07-05 19:18:58 +00:00
|
|
|
// ListObjectVersionsParams stores list objects versions parameters.
|
|
|
|
ListObjectVersionsParams struct {
|
2022-03-18 13:04:09 +00:00
|
|
|
BktInfo *data.BucketInfo
|
2021-07-05 19:18:58 +00:00
|
|
|
Delimiter string
|
|
|
|
KeyMarker string
|
|
|
|
MaxKeys int
|
|
|
|
Prefix string
|
|
|
|
VersionIDMarker string
|
|
|
|
Encode string
|
|
|
|
}
|
2020-08-03 11:48:33 +00:00
|
|
|
|
2021-09-07 06:17:12 +00:00
|
|
|
// VersionedObject stores info about objects to delete.
|
2021-08-10 12:08:15 +00:00
|
|
|
VersionedObject struct {
|
2021-09-07 06:17:12 +00:00
|
|
|
Name string
|
|
|
|
VersionID string
|
|
|
|
DeleteMarkVersion string
|
2022-03-31 06:24:57 +00:00
|
|
|
DeleteMarkerEtag string
|
2021-09-07 06:17:12 +00:00
|
|
|
Error error
|
2021-08-10 12:08:15 +00:00
|
|
|
}
|
|
|
|
|
2021-05-13 20:25:31 +00:00
|
|
|
// Client provides S3 API client interface.
|
2020-08-03 11:48:33 +00:00
|
|
|
Client interface {
|
2022-04-29 13:08:22 +00:00
|
|
|
Initialize(ctx context.Context, c EventListener) error
|
2021-10-19 15:08:07 +00:00
|
|
|
EphemeralKey() *keys.PublicKey
|
|
|
|
|
2022-02-28 08:02:05 +00:00
|
|
|
GetBucketSettings(ctx context.Context, bktInfo *data.BucketInfo) (*data.BucketSettings, error)
|
|
|
|
PutBucketSettings(ctx context.Context, p *PutSettingsParams) error
|
2021-08-09 14:29:44 +00:00
|
|
|
|
2021-10-04 14:32:35 +00:00
|
|
|
PutBucketCORS(ctx context.Context, p *PutCORSParams) error
|
2021-10-13 18:50:02 +00:00
|
|
|
GetBucketCORS(ctx context.Context, bktInfo *data.BucketInfo) (*data.CORSConfiguration, error)
|
2021-10-04 14:32:35 +00:00
|
|
|
DeleteBucketCORS(ctx context.Context, bktInfo *data.BucketInfo) error
|
|
|
|
|
2021-09-10 06:56:56 +00:00
|
|
|
ListBuckets(ctx context.Context) ([]*data.BucketInfo, error)
|
|
|
|
GetBucketInfo(ctx context.Context, name string) (*data.BucketInfo, error)
|
2022-03-18 13:04:09 +00:00
|
|
|
GetBucketACL(ctx context.Context, bktInfo *data.BucketInfo) (*BucketACL, error)
|
2021-07-21 11:59:46 +00:00
|
|
|
PutBucketACL(ctx context.Context, p *PutBucketACLParams) error
|
2022-03-18 13:04:09 +00:00
|
|
|
CreateBucket(ctx context.Context, p *CreateBucketParams) (*data.BucketInfo, error)
|
2021-06-23 20:25:00 +00:00
|
|
|
DeleteBucket(ctx context.Context, p *DeleteBucketParams) error
|
2020-08-03 11:48:33 +00:00
|
|
|
|
|
|
|
GetObject(ctx context.Context, p *GetObjectParams) error
|
2022-08-05 00:54:21 +00:00
|
|
|
GetObjectInfo(ctx context.Context, p *HeadObjectParams) (*data.ObjectInfo, error)
|
|
|
|
GetExtendedObjectInfo(ctx context.Context, p *HeadObjectParams) (*data.ExtendedObjectInfo, error)
|
2022-05-24 06:58:33 +00:00
|
|
|
|
2022-05-26 13:11:14 +00:00
|
|
|
GetLockInfo(ctx context.Context, obj *ObjectVersion) (*data.LockInfo, error)
|
2022-08-11 22:48:56 +00:00
|
|
|
PutLockInfo(ctx context.Context, p *PutLockInfoParams) error
|
2022-05-26 13:11:14 +00:00
|
|
|
|
2022-09-13 09:44:18 +00:00
|
|
|
GetBucketTagging(ctx context.Context, bktInfo *data.BucketInfo) (map[string]string, error)
|
|
|
|
PutBucketTagging(ctx context.Context, bktInfo *data.BucketInfo, tagSet map[string]string) error
|
|
|
|
DeleteBucketTagging(ctx context.Context, bktInfo *data.BucketInfo) error
|
2022-05-24 06:58:33 +00:00
|
|
|
|
2022-10-14 14:36:43 +00:00
|
|
|
GetObjectTagging(ctx context.Context, p *GetObjectTaggingParams) (string, map[string]string, error)
|
|
|
|
PutObjectTagging(ctx context.Context, p *PutObjectTaggingParams) (*data.NodeVersion, error)
|
2022-07-18 14:51:34 +00:00
|
|
|
DeleteObjectTagging(ctx context.Context, p *ObjectVersion) (*data.NodeVersion, error)
|
2020-08-03 11:48:33 +00:00
|
|
|
|
2022-10-14 14:36:43 +00:00
|
|
|
PutObject(ctx context.Context, p *PutObjectParams) (*data.ExtendedObjectInfo, error)
|
2020-08-03 11:48:33 +00:00
|
|
|
|
2022-10-14 14:36:43 +00:00
|
|
|
CopyObject(ctx context.Context, p *CopyObjectParams) (*data.ExtendedObjectInfo, error)
|
2020-08-03 11:48:33 +00:00
|
|
|
|
2021-07-18 13:40:19 +00:00
|
|
|
ListObjectsV1(ctx context.Context, p *ListObjectsParamsV1) (*ListObjectsInfoV1, error)
|
|
|
|
ListObjectsV2(ctx context.Context, p *ListObjectsParamsV2) (*ListObjectsInfoV2, error)
|
2021-07-05 19:18:58 +00:00
|
|
|
ListObjectVersions(ctx context.Context, p *ListObjectVersionsParams) (*ListObjectVersionsInfo, error)
|
2020-08-03 11:48:33 +00:00
|
|
|
|
2022-06-24 12:39:30 +00:00
|
|
|
DeleteObjects(ctx context.Context, p *DeleteObjectParams) []*VersionedObject
|
2021-11-25 15:05:58 +00:00
|
|
|
|
2022-05-23 14:34:13 +00:00
|
|
|
CreateMultipartUpload(ctx context.Context, p *CreateMultipartParams) error
|
2022-10-14 14:36:43 +00:00
|
|
|
CompleteMultipartUpload(ctx context.Context, p *CompleteMultipartParams) (*UploadData, *data.ExtendedObjectInfo, error)
|
2022-05-24 11:30:37 +00:00
|
|
|
UploadPart(ctx context.Context, p *UploadPartParams) (string, error)
|
2021-11-25 15:05:58 +00:00
|
|
|
UploadPartCopy(ctx context.Context, p *UploadCopyParams) (*data.ObjectInfo, error)
|
|
|
|
ListMultipartUploads(ctx context.Context, p *ListMultipartUploadsParams) (*ListMultipartUploadsInfo, error)
|
|
|
|
AbortMultipartUpload(ctx context.Context, p *UploadInfoParams) error
|
|
|
|
ListParts(ctx context.Context, p *ListPartsParams) (*ListPartsInfo, error)
|
2022-02-17 18:58:51 +00:00
|
|
|
|
|
|
|
PutBucketNotificationConfiguration(ctx context.Context, p *PutBucketNotificationConfigurationParams) error
|
|
|
|
GetBucketNotificationConfiguration(ctx context.Context, bktInfo *data.BucketInfo) (*data.NotificationConfiguration, error)
|
2022-05-31 14:27:08 +00:00
|
|
|
|
|
|
|
// Compound methods for optimizations
|
|
|
|
|
|
|
|
// GetObjectTaggingAndLock unifies GetObjectTagging and GetLock methods in single tree service invocation.
|
2022-06-28 13:35:05 +00:00
|
|
|
GetObjectTaggingAndLock(ctx context.Context, p *ObjectVersion, nodeVersion *data.NodeVersion) (map[string]string, *data.LockInfo, error)
|
2020-08-03 11:48:33 +00:00
|
|
|
}
|
|
|
|
)
|
|
|
|
|
2021-08-17 11:57:24 +00:00
|
|
|
const (
|
2022-05-26 13:11:14 +00:00
|
|
|
tagPrefix = "S3-Tag-"
|
2022-08-01 16:52:09 +00:00
|
|
|
|
|
|
|
AESEncryptionAlgorithm = "AES256"
|
|
|
|
AESKeySize = 32
|
2022-12-20 08:38:58 +00:00
|
|
|
AttributeEncryptionAlgorithm = api.FrostFSSystemMetadataPrefix + "Algorithm"
|
|
|
|
AttributeDecryptedSize = api.FrostFSSystemMetadataPrefix + "Decrypted-Size"
|
|
|
|
AttributeHMACSalt = api.FrostFSSystemMetadataPrefix + "HMAC-Salt"
|
|
|
|
AttributeHMACKey = api.FrostFSSystemMetadataPrefix + "HMAC-Key"
|
2022-08-17 11:18:36 +00:00
|
|
|
|
2022-12-20 08:38:58 +00:00
|
|
|
AttributeFrostfsCopiesNumber = "frostfs-copies-number" // such format to match X-Amz-Meta-Frostfs-Copies-Number header
|
2021-08-17 11:57:24 +00:00
|
|
|
)
|
2021-08-17 08:04:42 +00:00
|
|
|
|
2021-08-10 12:08:15 +00:00
|
|
|
func (t *VersionedObject) String() string {
|
|
|
|
return t.Name + ":" + t.VersionID
|
|
|
|
}
|
|
|
|
|
2022-03-05 06:58:54 +00:00
|
|
|
func (f MsgHandlerFunc) HandleMessage(ctx context.Context, msg *nats.Msg) error {
|
|
|
|
return f(ctx, msg)
|
|
|
|
}
|
|
|
|
|
2022-04-13 16:56:58 +00:00
|
|
|
// NewLayer creates an instance of a layer. It checks credentials
|
|
|
|
// and establishes gRPC connection with the node.
|
2022-12-20 08:38:58 +00:00
|
|
|
func NewLayer(log *zap.Logger, frostFS FrostFS, config *Config) Client {
|
2020-08-03 11:48:33 +00:00
|
|
|
return &layer{
|
2022-12-20 08:38:58 +00:00
|
|
|
frostFS: frostFS,
|
2021-09-10 06:56:56 +00:00
|
|
|
log: log,
|
2021-11-22 09:16:05 +00:00
|
|
|
anonKey: config.AnonKey,
|
2022-01-11 10:09:34 +00:00
|
|
|
resolver: config.Resolver,
|
2022-10-03 14:33:49 +00:00
|
|
|
cache: NewCache(config.Caches),
|
2022-04-22 07:18:21 +00:00
|
|
|
treeService: config.TreeService,
|
2020-11-24 07:01:38 +00:00
|
|
|
}
|
2020-08-03 11:48:33 +00:00
|
|
|
}
|
|
|
|
|
2021-10-19 15:08:07 +00:00
|
|
|
func (n *layer) EphemeralKey() *keys.PublicKey {
|
|
|
|
return n.anonKey.Key.PublicKey()
|
|
|
|
}
|
|
|
|
|
2022-04-29 13:08:22 +00:00
|
|
|
func (n *layer) Initialize(ctx context.Context, c EventListener) error {
|
2022-03-05 06:58:54 +00:00
|
|
|
if n.IsNotificationEnabled() {
|
|
|
|
return fmt.Errorf("already initialized")
|
|
|
|
}
|
|
|
|
|
2022-03-17 14:03:06 +00:00
|
|
|
// todo add notification handlers (e.g. for lifecycles)
|
2022-03-05 06:58:54 +00:00
|
|
|
|
|
|
|
c.Listen(ctx)
|
|
|
|
|
|
|
|
n.ncontroller = c
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-02-08 15:34:31 +00:00
|
|
|
func (n *layer) IsNotificationEnabled() bool {
|
|
|
|
return n.ncontroller != nil
|
|
|
|
}
|
|
|
|
|
2022-04-13 16:56:58 +00:00
|
|
|
// IsAuthenticatedRequest checks if access box exists in the current request.
|
2022-01-21 09:52:16 +00:00
|
|
|
func IsAuthenticatedRequest(ctx context.Context) bool {
|
|
|
|
_, ok := ctx.Value(api.BoxData).(*accessbox.Box)
|
|
|
|
return ok
|
|
|
|
}
|
|
|
|
|
2022-11-08 09:12:55 +00:00
|
|
|
// TimeNow returns client time from request or time.Now().
|
|
|
|
func TimeNow(ctx context.Context) time.Time {
|
|
|
|
if now, ok := ctx.Value(api.ClientTime).(time.Time); ok {
|
|
|
|
return now
|
|
|
|
}
|
|
|
|
|
|
|
|
return time.Now()
|
|
|
|
}
|
|
|
|
|
2020-11-27 12:36:15 +00:00
|
|
|
// Owner returns owner id from BearerToken (context) or from client owner.
|
2022-04-25 09:57:58 +00:00
|
|
|
func (n *layer) Owner(ctx context.Context) user.ID {
|
2022-06-01 14:00:30 +00:00
|
|
|
if bd, ok := ctx.Value(api.BoxData).(*accessbox.Box); ok && bd != nil && bd.Gate != nil && bd.Gate.BearerToken != nil {
|
|
|
|
return bearer.ResolveIssuer(*bd.Gate.BearerToken)
|
2020-11-27 12:36:15 +00:00
|
|
|
}
|
|
|
|
|
2022-04-25 09:57:58 +00:00
|
|
|
var ownerID user.ID
|
|
|
|
user.IDFromKey(&ownerID, (ecdsa.PublicKey)(*n.EphemeralKey()))
|
|
|
|
|
|
|
|
return ownerID
|
2020-11-27 12:36:15 +00:00
|
|
|
}
|
|
|
|
|
2022-06-06 11:09:09 +00:00
|
|
|
func (n *layer) prepareAuthParameters(ctx context.Context, prm *PrmAuth, bktOwner user.ID) {
|
2022-06-03 08:20:47 +00:00
|
|
|
if bd, ok := ctx.Value(api.BoxData).(*accessbox.Box); ok && bd != nil && bd.Gate != nil && bd.Gate.BearerToken != nil {
|
|
|
|
if bktOwner.Equals(bearer.ResolveIssuer(*bd.Gate.BearerToken)) {
|
2022-06-01 17:35:20 +00:00
|
|
|
prm.BearerToken = bd.Gate.BearerToken
|
|
|
|
return
|
|
|
|
}
|
2022-03-01 19:02:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
prm.PrivateKey = &n.anonKey.Key.PrivateKey
|
|
|
|
}
|
|
|
|
|
2021-05-20 10:14:17 +00:00
|
|
|
// GetBucketInfo returns bucket info by name.
|
2021-09-10 06:56:56 +00:00
|
|
|
func (n *layer) GetBucketInfo(ctx context.Context, name string) (*data.BucketInfo, error) {
|
2020-10-24 13:09:22 +00:00
|
|
|
name, err := url.QueryUnescape(name)
|
|
|
|
if err != nil {
|
2022-06-22 19:40:52 +00:00
|
|
|
return nil, fmt.Errorf("unescape bucket name: %w", err)
|
2020-10-24 13:09:22 +00:00
|
|
|
}
|
|
|
|
|
2022-10-03 14:33:49 +00:00
|
|
|
if bktInfo := n.cache.GetBucket(name); bktInfo != nil {
|
2021-08-18 13:48:58 +00:00
|
|
|
return bktInfo, nil
|
|
|
|
}
|
|
|
|
|
2021-11-22 09:16:05 +00:00
|
|
|
containerID, err := n.ResolveBucket(ctx, name)
|
|
|
|
if err != nil {
|
|
|
|
n.log.Debug("bucket not found", zap.Error(err))
|
2021-08-09 08:53:58 +00:00
|
|
|
return nil, errors.GetAPIError(errors.ErrNoSuchBucket)
|
2020-08-03 11:48:33 +00:00
|
|
|
}
|
|
|
|
|
2022-06-27 09:08:26 +00:00
|
|
|
return n.containerInfo(ctx, containerID)
|
2020-08-03 11:48:33 +00:00
|
|
|
}
|
|
|
|
|
2021-07-21 11:59:46 +00:00
|
|
|
// GetBucketACL returns bucket acl info by name.
|
2022-03-18 13:04:09 +00:00
|
|
|
func (n *layer) GetBucketACL(ctx context.Context, bktInfo *data.BucketInfo) (*BucketACL, error) {
|
|
|
|
eACL, err := n.GetContainerEACL(ctx, bktInfo.CID)
|
2021-07-21 11:59:46 +00:00
|
|
|
if err != nil {
|
2022-06-22 19:40:52 +00:00
|
|
|
return nil, fmt.Errorf("get container eacl: %w", err)
|
2021-07-21 11:59:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return &BucketACL{
|
2022-03-18 13:04:09 +00:00
|
|
|
Info: bktInfo,
|
2022-03-01 19:02:24 +00:00
|
|
|
EACL: eACL,
|
2021-07-21 11:59:46 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2022-04-13 16:56:58 +00:00
|
|
|
// PutBucketACL puts bucket acl by name.
|
2021-07-21 11:59:46 +00:00
|
|
|
func (n *layer) PutBucketACL(ctx context.Context, param *PutBucketACLParams) error {
|
2022-06-21 15:21:20 +00:00
|
|
|
return n.setContainerEACLTable(ctx, param.BktInfo.CID, param.EACL, param.SessionToken)
|
2021-07-21 11:59:46 +00:00
|
|
|
}
|
|
|
|
|
2022-04-13 16:56:58 +00:00
|
|
|
// ListBuckets returns all user containers. The name of the bucket is a container
|
2022-12-20 08:38:58 +00:00
|
|
|
// id. Timestamp is omitted since it is not saved in frostfs container.
|
2021-09-10 06:56:56 +00:00
|
|
|
func (n *layer) ListBuckets(ctx context.Context) ([]*data.BucketInfo, error) {
|
2020-08-03 11:48:33 +00:00
|
|
|
return n.containerList(ctx)
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetObject from storage.
|
|
|
|
func (n *layer) GetObject(ctx context.Context, p *GetObjectParams) error {
|
2022-02-08 16:54:04 +00:00
|
|
|
var params getParams
|
2020-08-03 11:48:33 +00:00
|
|
|
|
2022-05-31 12:38:06 +00:00
|
|
|
params.oid = p.ObjectInfo.ID
|
2022-06-02 16:56:04 +00:00
|
|
|
params.bktInfo = p.BucketInfo
|
2020-10-19 01:04:37 +00:00
|
|
|
|
2022-08-11 08:48:58 +00:00
|
|
|
var decReader *encryption.Decrypter
|
2022-08-01 16:52:09 +00:00
|
|
|
if p.Encryption.Enabled() {
|
|
|
|
var err error
|
2022-08-11 08:48:58 +00:00
|
|
|
decReader, err = getDecrypter(p)
|
2022-08-01 16:52:09 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("creating decrypter: %w", err)
|
|
|
|
}
|
2022-08-11 08:48:58 +00:00
|
|
|
params.off = decReader.EncryptedOffset()
|
|
|
|
params.ln = decReader.EncryptedLength()
|
2022-08-01 16:52:09 +00:00
|
|
|
} else {
|
|
|
|
if p.Range != nil {
|
|
|
|
if p.Range.Start > p.Range.End {
|
|
|
|
panic("invalid range")
|
|
|
|
}
|
|
|
|
params.ln = p.Range.End - p.Range.Start + 1
|
|
|
|
params.off = p.Range.Start
|
2022-02-08 16:54:04 +00:00
|
|
|
}
|
2021-06-24 11:10:00 +00:00
|
|
|
}
|
2020-08-03 11:48:33 +00:00
|
|
|
|
2022-03-02 16:09:02 +00:00
|
|
|
payload, err := n.initObjectPayloadReader(ctx, params)
|
2021-05-20 10:14:17 +00:00
|
|
|
if err != nil {
|
2022-03-02 16:09:02 +00:00
|
|
|
return fmt.Errorf("init object payload reader: %w", err)
|
|
|
|
}
|
|
|
|
|
2022-08-01 16:52:09 +00:00
|
|
|
bufSize := uint64(32 * 1024) // configure?
|
|
|
|
if params.ln != 0 && params.ln < bufSize {
|
|
|
|
bufSize = params.ln
|
2022-03-02 16:09:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// alloc buffer for copying
|
2022-08-01 16:52:09 +00:00
|
|
|
buf := make([]byte, bufSize) // sync-pool it?
|
|
|
|
|
|
|
|
r := payload
|
|
|
|
if decReader != nil {
|
|
|
|
if err = decReader.SetReader(payload); err != nil {
|
|
|
|
return fmt.Errorf("set reader to decrypter: %w", err)
|
|
|
|
}
|
2022-08-11 08:48:58 +00:00
|
|
|
r = io.LimitReader(decReader, int64(decReader.DecryptedLength()))
|
2022-08-01 16:52:09 +00:00
|
|
|
}
|
2022-03-02 16:09:02 +00:00
|
|
|
|
|
|
|
// copy full payload
|
2022-08-01 16:52:09 +00:00
|
|
|
written, err := io.CopyBuffer(p.Writer, r, buf)
|
2022-03-02 16:09:02 +00:00
|
|
|
if err != nil {
|
2022-09-07 12:41:45 +00:00
|
|
|
if decReader != nil {
|
|
|
|
return fmt.Errorf("copy object payload written: '%d', decLength: '%d', params.ln: '%d' : %w", written, decReader.DecryptedLength(), params.ln, err)
|
|
|
|
}
|
|
|
|
return fmt.Errorf("copy object payload written: '%d': %w", written, err)
|
2021-05-20 10:14:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2020-08-03 11:48:33 +00:00
|
|
|
}
|
|
|
|
|
2022-08-11 08:48:58 +00:00
|
|
|
func getDecrypter(p *GetObjectParams) (*encryption.Decrypter, error) {
|
|
|
|
var encRange *encryption.Range
|
|
|
|
if p.Range != nil {
|
|
|
|
encRange = &encryption.Range{Start: p.Range.Start, End: p.Range.End}
|
|
|
|
}
|
|
|
|
|
|
|
|
header := p.ObjectInfo.Headers[UploadCompletedParts]
|
|
|
|
if len(header) == 0 {
|
|
|
|
return encryption.NewDecrypter(p.Encryption, uint64(p.ObjectInfo.Size), encRange)
|
|
|
|
}
|
|
|
|
|
|
|
|
decryptedObjectSize, err := strconv.ParseUint(p.ObjectInfo.Headers[AttributeDecryptedSize], 10, 64)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("parse decrypted size: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
splits := strings.Split(header, ",")
|
|
|
|
sizes := make([]uint64, len(splits))
|
|
|
|
for i, splitInfo := range splits {
|
|
|
|
part, err := ParseCompletedPartHeader(splitInfo)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("parse completed part: %w", err)
|
|
|
|
}
|
|
|
|
sizes[i] = uint64(part.Size)
|
|
|
|
}
|
|
|
|
|
|
|
|
return encryption.NewMultipartDecrypter(p.Encryption, decryptedObjectSize, sizes, encRange)
|
|
|
|
}
|
|
|
|
|
2020-08-03 11:48:33 +00:00
|
|
|
// GetObjectInfo returns meta information about the object.
|
2022-08-05 00:54:21 +00:00
|
|
|
func (n *layer) GetObjectInfo(ctx context.Context, p *HeadObjectParams) (*data.ObjectInfo, error) {
|
|
|
|
extendedObjectInfo, err := n.GetExtendedObjectInfo(ctx, p)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return extendedObjectInfo.ObjectInfo, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetExtendedObjectInfo returns meta information and corresponding info from the tree service about the object.
|
|
|
|
func (n *layer) GetExtendedObjectInfo(ctx context.Context, p *HeadObjectParams) (*data.ExtendedObjectInfo, error) {
|
2021-08-10 10:03:09 +00:00
|
|
|
if len(p.VersionID) == 0 {
|
2022-03-18 13:04:09 +00:00
|
|
|
return n.headLastVersionIfNotDeleted(ctx, p.BktInfo, p.Object)
|
2021-08-10 12:08:15 +00:00
|
|
|
}
|
|
|
|
|
2022-03-18 13:04:09 +00:00
|
|
|
return n.headVersion(ctx, p.BktInfo, p)
|
2020-08-03 11:48:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// CopyObject from one bucket into another bucket.
|
2022-10-14 14:36:43 +00:00
|
|
|
func (n *layer) CopyObject(ctx context.Context, p *CopyObjectParams) (*data.ExtendedObjectInfo, error) {
|
2020-08-03 11:48:33 +00:00
|
|
|
pr, pw := io.Pipe()
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
err := n.GetObject(ctx, &GetObjectParams{
|
2021-08-10 10:03:09 +00:00
|
|
|
ObjectInfo: p.SrcObject,
|
|
|
|
Writer: pw,
|
2021-11-25 15:05:58 +00:00
|
|
|
Range: p.Range,
|
2022-06-02 16:56:04 +00:00
|
|
|
BucketInfo: p.ScrBktInfo,
|
2022-08-01 16:52:09 +00:00
|
|
|
Encryption: p.Encryption,
|
2020-08-03 11:48:33 +00:00
|
|
|
})
|
|
|
|
|
2020-10-24 13:09:22 +00:00
|
|
|
if err = pw.CloseWithError(err); err != nil {
|
|
|
|
n.log.Error("could not get object", zap.Error(err))
|
|
|
|
}
|
2020-08-03 11:48:33 +00:00
|
|
|
}()
|
|
|
|
|
|
|
|
return n.PutObject(ctx, &PutObjectParams{
|
2022-08-17 11:18:36 +00:00
|
|
|
BktInfo: p.DstBktInfo,
|
|
|
|
Object: p.DstObject,
|
|
|
|
Size: p.SrcSize,
|
|
|
|
Reader: pr,
|
|
|
|
Header: p.Header,
|
|
|
|
Encryption: p.Encryption,
|
|
|
|
CopiesNumber: p.CopiesNuber,
|
2020-08-03 11:48:33 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2022-06-27 09:33:36 +00:00
|
|
|
func getRandomOID() (oid.ID, error) {
|
2022-05-18 07:48:30 +00:00
|
|
|
b := [32]byte{}
|
|
|
|
if _, err := rand.Read(b[:]); err != nil {
|
2022-06-27 09:33:36 +00:00
|
|
|
return oid.ID{}, err
|
2022-05-18 07:48:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var objID oid.ID
|
|
|
|
objID.SetSHA256(b)
|
2022-06-27 09:33:36 +00:00
|
|
|
return objID, nil
|
2022-05-18 07:48:30 +00:00
|
|
|
}
|
|
|
|
|
2022-06-24 12:39:30 +00:00
|
|
|
func (n *layer) deleteObject(ctx context.Context, bkt *data.BucketInfo, settings *data.BucketSettings, obj *VersionedObject) *VersionedObject {
|
2022-07-19 14:58:18 +00:00
|
|
|
if len(obj.VersionID) != 0 || settings.Unversioned() {
|
2022-06-24 12:39:30 +00:00
|
|
|
var nodeVersion *data.NodeVersion
|
|
|
|
if nodeVersion, obj.Error = n.getNodeVersionToDelete(ctx, bkt, obj); obj.Error != nil {
|
2022-07-25 13:00:35 +00:00
|
|
|
return dismissNotFoundError(obj)
|
2022-06-15 12:17:29 +00:00
|
|
|
}
|
|
|
|
|
2022-06-24 12:39:30 +00:00
|
|
|
if obj.DeleteMarkVersion, obj.Error = n.removeOldVersion(ctx, bkt, nodeVersion, obj); obj.Error != nil {
|
2022-05-18 07:48:30 +00:00
|
|
|
return obj
|
|
|
|
}
|
2022-05-19 09:17:56 +00:00
|
|
|
|
2022-09-13 09:44:18 +00:00
|
|
|
obj.Error = n.treeService.RemoveVersion(ctx, bkt, nodeVersion.ID)
|
2022-10-03 14:33:49 +00:00
|
|
|
n.cache.CleanListCacheEntriesContainingObject(obj.Name, bkt.CID)
|
2022-06-24 12:39:30 +00:00
|
|
|
return obj
|
|
|
|
}
|
2022-05-19 09:17:56 +00:00
|
|
|
|
2022-06-24 12:39:30 +00:00
|
|
|
var newVersion *data.NodeVersion
|
2022-05-31 08:12:53 +00:00
|
|
|
|
2022-07-19 14:58:18 +00:00
|
|
|
if settings.VersioningSuspended() {
|
2022-08-08 22:35:26 +00:00
|
|
|
obj.VersionID = data.UnversionedObjectVersionID
|
2022-01-19 09:02:08 +00:00
|
|
|
|
2022-06-24 12:39:30 +00:00
|
|
|
var nodeVersion *data.NodeVersion
|
|
|
|
if nodeVersion, obj.Error = n.getNodeVersionToDelete(ctx, bkt, obj); obj.Error != nil {
|
2022-07-25 13:00:35 +00:00
|
|
|
return dismissNotFoundError(obj)
|
2022-06-15 12:17:29 +00:00
|
|
|
}
|
|
|
|
|
2022-07-25 13:00:35 +00:00
|
|
|
if obj.DeleteMarkVersion, obj.Error = n.removeOldVersion(ctx, bkt, nodeVersion, obj); obj.Error != nil {
|
2021-09-07 06:17:12 +00:00
|
|
|
return obj
|
2021-08-17 11:23:49 +00:00
|
|
|
}
|
2020-08-03 11:48:33 +00:00
|
|
|
}
|
|
|
|
|
2022-06-24 12:39:30 +00:00
|
|
|
randOID, err := getRandomOID()
|
|
|
|
if err != nil {
|
|
|
|
obj.Error = fmt.Errorf("couldn't get random oid: %w", err)
|
|
|
|
return obj
|
|
|
|
}
|
2022-07-05 08:04:21 +00:00
|
|
|
|
|
|
|
obj.DeleteMarkVersion = randOID.EncodeToString()
|
|
|
|
|
2022-06-24 12:39:30 +00:00
|
|
|
newVersion = &data.NodeVersion{
|
|
|
|
BaseNodeVersion: data.BaseNodeVersion{
|
|
|
|
OID: randOID,
|
|
|
|
FilePath: obj.Name,
|
|
|
|
},
|
|
|
|
DeleteMarker: &data.DeleteMarkerInfo{
|
2022-11-08 09:12:55 +00:00
|
|
|
Created: TimeNow(ctx),
|
2022-06-24 12:39:30 +00:00
|
|
|
Owner: n.Owner(ctx),
|
|
|
|
},
|
2022-07-19 14:58:18 +00:00
|
|
|
IsUnversioned: settings.VersioningSuspended(),
|
2022-06-24 12:39:30 +00:00
|
|
|
}
|
|
|
|
|
2022-09-13 09:44:18 +00:00
|
|
|
if _, obj.Error = n.treeService.AddVersion(ctx, bkt, newVersion); obj.Error != nil {
|
2022-06-24 12:39:30 +00:00
|
|
|
return obj
|
|
|
|
}
|
2022-07-05 06:25:23 +00:00
|
|
|
|
2022-10-03 14:33:49 +00:00
|
|
|
n.cache.DeleteObjectName(bkt.CID, bkt.Name, obj.Name)
|
2022-03-09 10:23:33 +00:00
|
|
|
|
2021-09-07 06:17:12 +00:00
|
|
|
return obj
|
2020-08-03 11:48:33 +00:00
|
|
|
}
|
|
|
|
|
2022-07-25 13:00:35 +00:00
|
|
|
func dismissNotFoundError(obj *VersionedObject) *VersionedObject {
|
|
|
|
if errors.IsS3Error(obj.Error, errors.ErrNoSuchKey) ||
|
|
|
|
errors.IsS3Error(obj.Error, errors.ErrNoSuchVersion) {
|
|
|
|
obj.Error = nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return obj
|
|
|
|
}
|
|
|
|
|
2022-06-24 12:39:30 +00:00
|
|
|
func (n *layer) getNodeVersionToDelete(ctx context.Context, bkt *data.BucketInfo, obj *VersionedObject) (*data.NodeVersion, error) {
|
|
|
|
objVersion := &ObjectVersion{
|
|
|
|
BktInfo: bkt,
|
|
|
|
ObjectName: obj.Name,
|
|
|
|
VersionID: obj.VersionID,
|
|
|
|
NoErrorOnDeleteMarker: true,
|
|
|
|
}
|
|
|
|
|
|
|
|
return n.getNodeVersion(ctx, objVersion)
|
|
|
|
}
|
|
|
|
|
2022-06-15 12:17:29 +00:00
|
|
|
func (n *layer) removeOldVersion(ctx context.Context, bkt *data.BucketInfo, nodeVersion *data.NodeVersion, obj *VersionedObject) (string, error) {
|
2022-08-09 12:10:04 +00:00
|
|
|
if nodeVersion.IsDeleteMarker() {
|
2022-06-15 12:17:29 +00:00
|
|
|
return obj.VersionID, nil
|
2022-05-19 09:17:56 +00:00
|
|
|
}
|
|
|
|
|
2022-06-15 12:17:29 +00:00
|
|
|
return "", n.objectDelete(ctx, bkt, nodeVersion.OID)
|
2022-05-18 06:51:12 +00:00
|
|
|
}
|
|
|
|
|
2020-08-03 11:48:33 +00:00
|
|
|
// DeleteObjects from the storage.
|
2022-06-24 12:39:30 +00:00
|
|
|
func (n *layer) DeleteObjects(ctx context.Context, p *DeleteObjectParams) []*VersionedObject {
|
2022-03-31 06:24:29 +00:00
|
|
|
for i, obj := range p.Objects {
|
2022-06-24 12:39:30 +00:00
|
|
|
p.Objects[i] = n.deleteObject(ctx, p.BktInfo, p.Settings, obj)
|
2020-08-03 11:48:33 +00:00
|
|
|
}
|
|
|
|
|
2022-06-24 12:39:30 +00:00
|
|
|
return p.Objects
|
2020-08-03 11:48:33 +00:00
|
|
|
}
|
2021-06-23 20:21:15 +00:00
|
|
|
|
2022-03-18 13:04:09 +00:00
|
|
|
func (n *layer) CreateBucket(ctx context.Context, p *CreateBucketParams) (*data.BucketInfo, error) {
|
2022-02-15 09:06:00 +00:00
|
|
|
bktInfo, err := n.GetBucketInfo(ctx, p.Name)
|
2021-07-07 14:56:29 +00:00
|
|
|
if err != nil {
|
2021-08-09 08:53:58 +00:00
|
|
|
if errors.IsS3Error(err, errors.ErrNoSuchBucket) {
|
2021-07-07 14:56:29 +00:00
|
|
|
return n.createContainer(ctx, p)
|
|
|
|
}
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-06-21 15:21:20 +00:00
|
|
|
if p.SessionContainerCreation != nil && session.IssuedBy(*p.SessionContainerCreation, bktInfo.Owner) {
|
2022-02-15 09:06:00 +00:00
|
|
|
return nil, errors.GetAPIError(errors.ErrBucketAlreadyOwnedByYou)
|
|
|
|
}
|
|
|
|
|
2021-08-09 08:53:58 +00:00
|
|
|
return nil, errors.GetAPIError(errors.ErrBucketAlreadyExists)
|
2021-06-23 20:21:15 +00:00
|
|
|
}
|
2021-06-23 20:25:00 +00:00
|
|
|
|
2022-06-27 09:08:26 +00:00
|
|
|
func (n *layer) ResolveBucket(ctx context.Context, name string) (cid.ID, error) {
|
2022-04-25 09:57:58 +00:00
|
|
|
var cnrID cid.ID
|
|
|
|
if err := cnrID.DecodeString(name); err != nil {
|
2022-01-11 10:09:34 +00:00
|
|
|
return n.resolver.Resolve(ctx, name)
|
2021-11-22 09:16:05 +00:00
|
|
|
}
|
|
|
|
|
2022-06-27 09:08:26 +00:00
|
|
|
return cnrID, nil
|
2021-11-22 09:16:05 +00:00
|
|
|
}
|
|
|
|
|
2021-06-23 20:25:00 +00:00
|
|
|
func (n *layer) DeleteBucket(ctx context.Context, p *DeleteBucketParams) error {
|
2022-07-21 09:05:47 +00:00
|
|
|
nodeVersions, err := n.bucketNodeVersions(ctx, p.BktInfo, "")
|
2021-08-10 12:08:15 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-07-21 09:05:47 +00:00
|
|
|
if len(nodeVersions) != 0 {
|
2021-08-11 10:02:13 +00:00
|
|
|
return errors.GetAPIError(errors.ErrBucketNotEmpty)
|
2021-08-10 12:08:15 +00:00
|
|
|
}
|
|
|
|
|
2022-10-03 14:33:49 +00:00
|
|
|
n.cache.DeleteBucket(p.BktInfo.Name)
|
2022-12-20 08:38:58 +00:00
|
|
|
return n.frostFS.DeleteContainer(ctx, p.BktInfo.CID, p.SessionToken)
|
2021-08-09 14:29:44 +00:00
|
|
|
}
|