forked from TrueCloudLab/frostfs-s3-gw
parent
11558124cd
commit
4bb885d526
21 changed files with 550 additions and 466 deletions
21
api/cache/bucket.go → api/cache/buckets.go
vendored
21
api/cache/bucket.go → api/cache/buckets.go
vendored
|
@ -18,10 +18,11 @@ type (
|
||||||
|
|
||||||
// BucketInfo stores basic bucket data.
|
// BucketInfo stores basic bucket data.
|
||||||
BucketInfo struct {
|
BucketInfo struct {
|
||||||
Name string
|
Name string
|
||||||
CID *cid.ID
|
CID *cid.ID
|
||||||
Owner *owner.ID
|
Owner *owner.ID
|
||||||
Created time.Time
|
Created time.Time
|
||||||
|
BasicACL uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBucketCache contains cache with objects and lifetime of cache entries.
|
// GetBucketCache contains cache with objects and lifetime of cache entries.
|
||||||
|
@ -62,3 +63,15 @@ func (o *GetBucketCache) Put(bkt *BucketInfo) error {
|
||||||
func (o *GetBucketCache) Delete(key string) bool {
|
func (o *GetBucketCache) Delete(key string) bool {
|
||||||
return o.cache.Remove(key)
|
return o.cache.Remove(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bktVersionSettingsObject = ".s3-versioning-settings"
|
||||||
|
|
||||||
|
// SettingsObjectName is system name for bucket settings file.
|
||||||
|
func (b *BucketInfo) SettingsObjectName() string {
|
||||||
|
return bktVersionSettingsObject
|
||||||
|
}
|
||||||
|
|
||||||
|
// SettingsObjectKey is key to use in SystemCache.
|
||||||
|
func (b *BucketInfo) SettingsObjectKey() string {
|
||||||
|
return b.Name + bktVersionSettingsObject
|
||||||
|
}
|
22
api/cache/head_cache.go → api/cache/names.go
vendored
22
api/cache/head_cache.go → api/cache/names.go
vendored
|
@ -7,30 +7,32 @@ import (
|
||||||
"github.com/nspcc-dev/neofs-api-go/pkg/object"
|
"github.com/nspcc-dev/neofs-api-go/pkg/object"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HeadObjectsCache provides interface for lru cache for objects.
|
// ObjectsNameCache provides interface for lru cache for objects.
|
||||||
type HeadObjectsCache interface {
|
// This cache contains mapping nice name to object addresses.
|
||||||
|
// Key is bucketName+objectName.
|
||||||
|
type ObjectsNameCache interface {
|
||||||
Get(key string) *object.Address
|
Get(key string) *object.Address
|
||||||
Put(key string, address *object.Address) error
|
Put(key string, address *object.Address) error
|
||||||
Delete(key string) bool
|
Delete(key string) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// HeadObjectCache contains cache with objects and lifetime of cache entries.
|
// NameCache contains cache with objects and lifetime of cache entries.
|
||||||
HeadObjectCache struct {
|
NameCache struct {
|
||||||
cache gcache.Cache
|
cache gcache.Cache
|
||||||
lifetime time.Duration
|
lifetime time.Duration
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewHeadObject creates an object of ObjectHeadersCache.
|
// NewObjectsNameCache creates an object of ObjectsNameCache.
|
||||||
func NewHeadObject(cacheSize int, lifetime time.Duration) *HeadObjectCache {
|
func NewObjectsNameCache(cacheSize int, lifetime time.Duration) *NameCache {
|
||||||
gc := gcache.New(cacheSize).LRU().Build()
|
gc := gcache.New(cacheSize).LRU().Build()
|
||||||
|
|
||||||
return &HeadObjectCache{cache: gc, lifetime: lifetime}
|
return &NameCache{cache: gc, lifetime: lifetime}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns cached object.
|
// Get returns cached object.
|
||||||
func (o *HeadObjectCache) Get(key string) *object.Address {
|
func (o *NameCache) Get(key string) *object.Address {
|
||||||
entry, err := o.cache.Get(key)
|
entry, err := o.cache.Get(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
|
@ -45,11 +47,11 @@ func (o *HeadObjectCache) Get(key string) *object.Address {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put puts an object to cache.
|
// Put puts an object to cache.
|
||||||
func (o *HeadObjectCache) Put(key string, address *object.Address) error {
|
func (o *NameCache) Put(key string, address *object.Address) error {
|
||||||
return o.cache.SetWithExpire(key, address, o.lifetime)
|
return o.cache.SetWithExpire(key, address, o.lifetime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete deletes an object from cache.
|
// Delete deletes an object from cache.
|
||||||
func (o *HeadObjectCache) Delete(key string) bool {
|
func (o *NameCache) Delete(key string) bool {
|
||||||
return o.cache.Remove(key)
|
return o.cache.Remove(key)
|
||||||
}
|
}
|
1
api/cache/object_cache_test.go
vendored
1
api/cache/object_cache_test.go
vendored
|
@ -5,7 +5,6 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neofs-api-go/pkg/object"
|
"github.com/nspcc-dev/neofs-api-go/pkg/object"
|
||||||
|
|
||||||
objecttest "github.com/nspcc-dev/neofs-api-go/pkg/object/test"
|
objecttest "github.com/nspcc-dev/neofs-api-go/pkg/object/test"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
19
api/cache/system.go
vendored
19
api/cache/system.go
vendored
|
@ -3,35 +3,36 @@ package cache
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neofs-api-go/pkg/object"
|
|
||||||
|
|
||||||
"github.com/bluele/gcache"
|
"github.com/bluele/gcache"
|
||||||
|
"github.com/nspcc-dev/neofs-api-go/pkg/object"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// SystemCache provides interface for lru cache for objects.
|
// SystemCache provides interface for lru cache for objects.
|
||||||
|
// This cache contains "system" objects (bucket versioning settings, tagging object etc.).
|
||||||
|
// Key is bucketName+systemFileName.
|
||||||
SystemCache interface {
|
SystemCache interface {
|
||||||
Get(key string) *object.Object
|
Get(key string) *object.Object
|
||||||
Put(key string, obj *object.Object) error
|
Put(key string, obj *object.Object) error
|
||||||
Delete(key string) bool
|
Delete(key string) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// systemCache contains cache with objects and lifetime of cache entries.
|
// SysCache contains cache with objects and lifetime of cache entries.
|
||||||
systemCache struct {
|
SysCache struct {
|
||||||
cache gcache.Cache
|
cache gcache.Cache
|
||||||
lifetime time.Duration
|
lifetime time.Duration
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewSystemCache creates an object of SystemCache.
|
// NewSystemCache creates an object of SystemCache.
|
||||||
func NewSystemCache(cacheSize int, lifetime time.Duration) SystemCache {
|
func NewSystemCache(cacheSize int, lifetime time.Duration) *SysCache {
|
||||||
gc := gcache.New(cacheSize).LRU().Build()
|
gc := gcache.New(cacheSize).LRU().Build()
|
||||||
|
|
||||||
return &systemCache{cache: gc, lifetime: lifetime}
|
return &SysCache{cache: gc, lifetime: lifetime}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns cached object.
|
// Get returns cached object.
|
||||||
func (o *systemCache) Get(key string) *object.Object {
|
func (o *SysCache) Get(key string) *object.Object {
|
||||||
entry, err := o.cache.Get(key)
|
entry, err := o.cache.Get(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
|
@ -46,11 +47,11 @@ func (o *systemCache) Get(key string) *object.Object {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put puts an object to cache.
|
// Put puts an object to cache.
|
||||||
func (o *systemCache) Put(key string, obj *object.Object) error {
|
func (o *SysCache) Put(key string, obj *object.Object) error {
|
||||||
return o.cache.SetWithExpire(key, obj, o.lifetime)
|
return o.cache.SetWithExpire(key, obj, o.lifetime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete deletes an object from cache.
|
// Delete deletes an object from cache.
|
||||||
func (o *systemCache) Delete(key string) bool {
|
func (o *SysCache) Delete(key string) bool {
|
||||||
return o.cache.Remove(key)
|
return o.cache.Remove(key)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1924,26 +1924,18 @@ func GetAPIError(code ErrorCode) Error {
|
||||||
return errorCodes.toAPIErr(ErrInternalError)
|
return errorCodes.toAPIErr(ErrInternalError)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenericError - generic object layer error.
|
// ObjectError - error that linked to specific object.
|
||||||
type GenericError struct {
|
type ObjectError struct {
|
||||||
Bucket string
|
Err error
|
||||||
Object string
|
Object string
|
||||||
|
Version string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObjectAlreadyExists object already exists.
|
func (e ObjectError) Error() string {
|
||||||
// This type should be removed when s3-gw will support versioning.
|
return fmt.Sprintf("%s (%s:%s)", e.Err, e.Object, e.Version)
|
||||||
type ObjectAlreadyExists GenericError
|
|
||||||
|
|
||||||
func (e ObjectAlreadyExists) Error() string {
|
|
||||||
return "Object: " + e.Bucket + "#" + e.Object + " already exists"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteError - returns when cant remove object.
|
// ObjectVersion get "object:version" string.
|
||||||
type DeleteError struct {
|
func (e ObjectError) ObjectVersion() string {
|
||||||
Err error
|
return e.Object + ":" + e.Version
|
||||||
Object string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e DeleteError) Error() string {
|
|
||||||
return fmt.Sprintf("%s (%s)", e.Err, e.Object)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/nspcc-dev/neofs-api-go/pkg/acl/eacl"
|
"github.com/nspcc-dev/neofs-api-go/pkg/acl/eacl"
|
||||||
"github.com/nspcc-dev/neofs-api-go/pkg/object"
|
"github.com/nspcc-dev/neofs-api-go/pkg/object"
|
||||||
"github.com/nspcc-dev/neofs-s3-gw/api"
|
"github.com/nspcc-dev/neofs-s3-gw/api"
|
||||||
|
"github.com/nspcc-dev/neofs-s3-gw/api/cache"
|
||||||
"github.com/nspcc-dev/neofs-s3-gw/api/errors"
|
"github.com/nspcc-dev/neofs-s3-gw/api/errors"
|
||||||
"github.com/nspcc-dev/neofs-s3-gw/api/layer"
|
"github.com/nspcc-dev/neofs-s3-gw/api/layer"
|
||||||
)
|
)
|
||||||
|
@ -239,7 +240,13 @@ func (h *handler) PutObjectACLHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = h.obj.GetObjectInfo(r.Context(), reqInfo.BucketName, reqInfo.ObjectName); err != nil {
|
p := &layer.HeadObjectParams{
|
||||||
|
Bucket: reqInfo.BucketName,
|
||||||
|
Object: reqInfo.ObjectName,
|
||||||
|
VersionID: reqInfo.URL.Query().Get(api.QueryVersionID),
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = h.obj.GetObjectInfo(r.Context(), p); err != nil {
|
||||||
h.logAndSendError(w, "could not get object info", reqInfo, err)
|
h.logAndSendError(w, "could not get object info", reqInfo, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -273,7 +280,7 @@ func (h *handler) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkOwner(info *layer.BucketInfo, owner string) error {
|
func checkOwner(info *cache.BucketInfo, owner string) error {
|
||||||
if owner == "" {
|
if owner == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,10 +32,11 @@ func path2BucketObject(path string) (bucket, prefix string) {
|
||||||
func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
|
func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
inf *layer.ObjectInfo
|
info *layer.ObjectInfo
|
||||||
metadata map[string]string
|
metadata map[string]string
|
||||||
|
|
||||||
reqInfo = api.GetReqInfo(r.Context())
|
reqInfo = api.GetReqInfo(r.Context())
|
||||||
|
versionID string
|
||||||
)
|
)
|
||||||
|
|
||||||
src := r.Header.Get("X-Amz-Copy-Source")
|
src := r.Header.Get("X-Amz-Copy-Source")
|
||||||
|
@ -45,14 +46,7 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
// of the version ID to null. If you have enabled versioning, Amazon S3 assigns a
|
// of the version ID to null. If you have enabled versioning, Amazon S3 assigns a
|
||||||
// unique version ID value for the object.
|
// unique version ID value for the object.
|
||||||
if u, err := url.Parse(src); err == nil {
|
if u, err := url.Parse(src); err == nil {
|
||||||
// Check if versionId query param was added, if yes then check if
|
versionID = u.Query().Get(api.QueryVersionID)
|
||||||
// its non "null" value, we should error out since we do not support
|
|
||||||
// any versions other than "null".
|
|
||||||
if vid := u.Query().Get("versionId"); vid != "" && vid != "null" {
|
|
||||||
h.logAndSendError(w, "no such version", reqInfo, errors.GetAPIError(errors.ErrNoSuchVersion))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
src = u.Path
|
src = u.Path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,7 +60,7 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
p := &layer.HeadObjectParams{
|
p := &layer.HeadObjectParams{
|
||||||
Bucket: srcBucket,
|
Bucket: srcBucket,
|
||||||
Object: srcObject,
|
Object: srcObject,
|
||||||
VersionID: reqInfo.URL.Query().Get("versionId"),
|
VersionID: versionID,
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.MetadataDirective == replaceMetadataDirective {
|
if args.MetadataDirective == replaceMetadataDirective {
|
||||||
|
@ -85,46 +79,46 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if inf, err = h.obj.GetObjectInfo(r.Context(), p); err != nil {
|
if info, err = h.obj.GetObjectInfo(r.Context(), p); err != nil {
|
||||||
h.logAndSendError(w, "could not find object", reqInfo, err)
|
h.logAndSendError(w, "could not find object", reqInfo, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = checkPreconditions(inf, args.Conditional); err != nil {
|
if err = checkPreconditions(info, args.Conditional); err != nil {
|
||||||
h.logAndSendError(w, "precondition failed", reqInfo, errors.GetAPIError(errors.ErrPreconditionFailed))
|
h.logAndSendError(w, "precondition failed", reqInfo, errors.GetAPIError(errors.ErrPreconditionFailed))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if metadata == nil {
|
if metadata == nil {
|
||||||
if len(inf.ContentType) > 0 {
|
if len(info.ContentType) > 0 {
|
||||||
inf.Headers[api.ContentType] = inf.ContentType
|
info.Headers[api.ContentType] = info.ContentType
|
||||||
}
|
}
|
||||||
metadata = inf.Headers
|
metadata = info.Headers
|
||||||
} else if contentType := r.Header.Get(api.ContentType); len(contentType) > 0 {
|
} else if contentType := r.Header.Get(api.ContentType); len(contentType) > 0 {
|
||||||
metadata[api.ContentType] = contentType
|
metadata[api.ContentType] = contentType
|
||||||
}
|
}
|
||||||
|
|
||||||
params := &layer.CopyObjectParams{
|
params := &layer.CopyObjectParams{
|
||||||
SrcObject: inf,
|
SrcObject: info,
|
||||||
DstBucket: reqInfo.BucketName,
|
DstBucket: reqInfo.BucketName,
|
||||||
DstObject: reqInfo.ObjectName,
|
DstObject: reqInfo.ObjectName,
|
||||||
SrcSize: inf.Size,
|
SrcSize: info.Size,
|
||||||
Header: metadata,
|
Header: metadata,
|
||||||
}
|
}
|
||||||
|
|
||||||
additional := []zap.Field{zap.String("src_bucket_name", srcBucket), zap.String("src_object_name", srcObject)}
|
additional := []zap.Field{zap.String("src_bucket_name", srcBucket), zap.String("src_object_name", srcObject)}
|
||||||
if inf, err = h.obj.CopyObject(r.Context(), params); err != nil {
|
if info, err = h.obj.CopyObject(r.Context(), params); err != nil {
|
||||||
h.logAndSendError(w, "couldn't copy object", reqInfo, err, additional...)
|
h.logAndSendError(w, "couldn't copy object", reqInfo, err, additional...)
|
||||||
return
|
return
|
||||||
} else if err = api.EncodeToResponse(w, &CopyObjectResponse{LastModified: inf.Created.Format(time.RFC3339), ETag: inf.HashSum}); err != nil {
|
} else if err = api.EncodeToResponse(w, &CopyObjectResponse{LastModified: info.Created.Format(time.RFC3339), ETag: info.HashSum}); err != nil {
|
||||||
h.logAndSendError(w, "something went wrong", reqInfo, err, additional...)
|
h.logAndSendError(w, "something went wrong", reqInfo, err, additional...)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h.log.Info("object is copied",
|
h.log.Info("object is copied",
|
||||||
zap.String("bucket", inf.Bucket),
|
zap.String("bucket", info.Bucket),
|
||||||
zap.String("object", inf.Name),
|
zap.String("object", info.Name),
|
||||||
zap.Stringer("object_id", inf.ID()))
|
zap.Stringer("object_id", info.ID()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseCopyObjectArgs(headers http.Header) (*copyObjectArgs, error) {
|
func parseCopyObjectArgs(headers http.Header) (*copyObjectArgs, error) {
|
||||||
|
|
|
@ -27,9 +27,10 @@ type ObjectIdentifier struct {
|
||||||
|
|
||||||
// DeleteError structure.
|
// DeleteError structure.
|
||||||
type DeleteError struct {
|
type DeleteError struct {
|
||||||
Code string
|
Code string
|
||||||
Message string
|
Message string
|
||||||
Key string
|
Key string
|
||||||
|
VersionID string `xml:"versionId,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteObjectsResponse container for multiple object deletes.
|
// DeleteObjectsResponse container for multiple object deletes.
|
||||||
|
@ -47,7 +48,7 @@ func (h *handler) DeleteObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
reqInfo := api.GetReqInfo(r.Context())
|
reqInfo := api.GetReqInfo(r.Context())
|
||||||
versionedObject := []*layer.VersionedObject{{
|
versionedObject := []*layer.VersionedObject{{
|
||||||
Name: reqInfo.ObjectName,
|
Name: reqInfo.ObjectName,
|
||||||
VersionID: reqInfo.URL.Query().Get("versionId"),
|
VersionID: reqInfo.URL.Query().Get(api.QueryVersionID),
|
||||||
}}
|
}}
|
||||||
|
|
||||||
if err := h.checkBucketOwner(r, reqInfo.BucketName); err != nil {
|
if err := h.checkBucketOwner(r, reqInfo.BucketName); err != nil {
|
||||||
|
@ -99,7 +100,7 @@ func (h *handler) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *http.Re
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
removed := make(map[string]struct{})
|
removed := make(map[string]*layer.VersionedObject)
|
||||||
toRemove := make([]*layer.VersionedObject, 0, len(requested.Objects))
|
toRemove := make([]*layer.VersionedObject, 0, len(requested.Objects))
|
||||||
for _, obj := range requested.Objects {
|
for _, obj := range requested.Objects {
|
||||||
versionedObj := &layer.VersionedObject{
|
versionedObj := &layer.VersionedObject{
|
||||||
|
@ -107,7 +108,7 @@ func (h *handler) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *http.Re
|
||||||
VersionID: obj.VersionID,
|
VersionID: obj.VersionID,
|
||||||
}
|
}
|
||||||
toRemove = append(toRemove, versionedObj)
|
toRemove = append(toRemove, versionedObj)
|
||||||
removed[versionedObj.String()] = struct{}{}
|
removed[versionedObj.String()] = versionedObj
|
||||||
}
|
}
|
||||||
|
|
||||||
response := &DeleteObjectsResponse{
|
response := &DeleteObjectsResponse{
|
||||||
|
@ -135,23 +136,26 @@ func (h *handler) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *http.Re
|
||||||
h.logAndSendError(w, "could not delete objects", reqInfo, nil, additional...)
|
h.logAndSendError(w, "could not delete objects", reqInfo, nil, additional...)
|
||||||
|
|
||||||
for _, e := range errs {
|
for _, e := range errs {
|
||||||
if err, ok := e.(*errors.DeleteError); ok {
|
if err, ok := e.(*errors.ObjectError); ok {
|
||||||
code := "BadRequest"
|
code := "BadRequest"
|
||||||
desc := err.Error()
|
if s3err, ok := err.Err.(errors.Error); ok {
|
||||||
|
code = s3err.Code
|
||||||
|
}
|
||||||
|
|
||||||
response.Errors = append(response.Errors, DeleteError{
|
response.Errors = append(response.Errors, DeleteError{
|
||||||
Code: code,
|
Code: code,
|
||||||
Message: desc,
|
Message: err.Error(),
|
||||||
Key: err.Object,
|
Key: err.Object,
|
||||||
|
VersionID: err.Version,
|
||||||
})
|
})
|
||||||
|
|
||||||
delete(removed, err.Object)
|
delete(removed, err.ObjectVersion())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for key := range removed {
|
for _, val := range removed {
|
||||||
response.DeletedObjects = append(response.DeletedObjects, ObjectIdentifier{ObjectName: key})
|
response.DeletedObjects = append(response.DeletedObjects, ObjectIdentifier{ObjectName: val.Name, VersionID: val.VersionID})
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := api.EncodeToResponse(w, response); err != nil {
|
if err := api.EncodeToResponse(w, response); err != nil {
|
||||||
|
|
|
@ -82,7 +82,7 @@ func writeHeaders(h http.Header, info *layer.ObjectInfo) {
|
||||||
func (h *handler) GetObjectHandler(w http.ResponseWriter, r *http.Request) {
|
func (h *handler) GetObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
inf *layer.ObjectInfo
|
info *layer.ObjectInfo
|
||||||
params *layer.RangeParams
|
params *layer.RangeParams
|
||||||
|
|
||||||
reqInfo = api.GetReqInfo(r.Context())
|
reqInfo = api.GetReqInfo(r.Context())
|
||||||
|
@ -102,30 +102,30 @@ func (h *handler) GetObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
p := &layer.HeadObjectParams{
|
p := &layer.HeadObjectParams{
|
||||||
Bucket: reqInfo.BucketName,
|
Bucket: reqInfo.BucketName,
|
||||||
Object: reqInfo.ObjectName,
|
Object: reqInfo.ObjectName,
|
||||||
VersionID: reqInfo.URL.Query().Get("versionId"),
|
VersionID: reqInfo.URL.Query().Get(api.QueryVersionID),
|
||||||
}
|
}
|
||||||
|
|
||||||
if inf, err = h.obj.GetObjectInfo(r.Context(), p); err != nil {
|
if info, err = h.obj.GetObjectInfo(r.Context(), p); err != nil {
|
||||||
h.logAndSendError(w, "could not find object", reqInfo, err)
|
h.logAndSendError(w, "could not find object", reqInfo, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = checkPreconditions(inf, args.Conditional); err != nil {
|
if err = checkPreconditions(info, args.Conditional); err != nil {
|
||||||
h.logAndSendError(w, "precondition failed", reqInfo, err)
|
h.logAndSendError(w, "precondition failed", reqInfo, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if params, err = fetchRangeHeader(r.Header, uint64(inf.Size)); err != nil {
|
if params, err = fetchRangeHeader(r.Header, uint64(info.Size)); err != nil {
|
||||||
h.logAndSendError(w, "could not parse range header", reqInfo, err)
|
h.logAndSendError(w, "could not parse range header", reqInfo, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeHeaders(w.Header(), inf)
|
writeHeaders(w.Header(), info)
|
||||||
if params != nil {
|
if params != nil {
|
||||||
writeRangeHeaders(w, params, inf.Size)
|
writeRangeHeaders(w, params, info.Size)
|
||||||
}
|
}
|
||||||
|
|
||||||
getParams := &layer.GetObjectParams{
|
getParams := &layer.GetObjectParams{
|
||||||
ObjectInfo: inf,
|
ObjectInfo: info,
|
||||||
Writer: w,
|
Writer: w,
|
||||||
Range: params,
|
Range: params,
|
||||||
VersionID: p.VersionID,
|
VersionID: p.VersionID,
|
||||||
|
@ -135,17 +135,17 @@ func (h *handler) GetObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkPreconditions(inf *layer.ObjectInfo, args *conditionalArgs) error {
|
func checkPreconditions(info *layer.ObjectInfo, args *conditionalArgs) error {
|
||||||
if len(args.IfMatch) > 0 && args.IfMatch != inf.HashSum {
|
if len(args.IfMatch) > 0 && args.IfMatch != info.HashSum {
|
||||||
return errors.GetAPIError(errors.ErrPreconditionFailed)
|
return errors.GetAPIError(errors.ErrPreconditionFailed)
|
||||||
}
|
}
|
||||||
if len(args.IfNoneMatch) > 0 && args.IfNoneMatch == inf.HashSum {
|
if len(args.IfNoneMatch) > 0 && args.IfNoneMatch == info.HashSum {
|
||||||
return errors.GetAPIError(errors.ErrNotModified)
|
return errors.GetAPIError(errors.ErrNotModified)
|
||||||
}
|
}
|
||||||
if args.IfModifiedSince != nil && inf.Created.Before(*args.IfModifiedSince) {
|
if args.IfModifiedSince != nil && info.Created.Before(*args.IfModifiedSince) {
|
||||||
return errors.GetAPIError(errors.ErrNotModified)
|
return errors.GetAPIError(errors.ErrNotModified)
|
||||||
}
|
}
|
||||||
if args.IfUnmodifiedSince != nil && inf.Created.After(*args.IfUnmodifiedSince) {
|
if args.IfUnmodifiedSince != nil && info.Created.After(*args.IfUnmodifiedSince) {
|
||||||
if len(args.IfMatch) == 0 {
|
if len(args.IfMatch) == 0 {
|
||||||
return errors.GetAPIError(errors.ErrPreconditionFailed)
|
return errors.GetAPIError(errors.ErrPreconditionFailed)
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,8 +25,8 @@ func getRangeToDetectContentType(maxSize int64) *layer.RangeParams {
|
||||||
|
|
||||||
func (h *handler) HeadObjectHandler(w http.ResponseWriter, r *http.Request) {
|
func (h *handler) HeadObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
var (
|
var (
|
||||||
err error
|
err error
|
||||||
inf *layer.ObjectInfo
|
info *layer.ObjectInfo
|
||||||
|
|
||||||
reqInfo = api.GetReqInfo(r.Context())
|
reqInfo = api.GetReqInfo(r.Context())
|
||||||
)
|
)
|
||||||
|
@ -39,30 +39,30 @@ func (h *handler) HeadObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
p := &layer.HeadObjectParams{
|
p := &layer.HeadObjectParams{
|
||||||
Bucket: reqInfo.BucketName,
|
Bucket: reqInfo.BucketName,
|
||||||
Object: reqInfo.ObjectName,
|
Object: reqInfo.ObjectName,
|
||||||
VersionID: reqInfo.URL.Query().Get("versionId"),
|
VersionID: reqInfo.URL.Query().Get(api.QueryVersionID),
|
||||||
}
|
}
|
||||||
|
|
||||||
if inf, err = h.obj.GetObjectInfo(r.Context(), p); err != nil {
|
if info, err = h.obj.GetObjectInfo(r.Context(), p); err != nil {
|
||||||
h.logAndSendError(w, "could not fetch object info", reqInfo, err)
|
h.logAndSendError(w, "could not fetch object info", reqInfo, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(inf.ContentType) == 0 {
|
if len(info.ContentType) == 0 {
|
||||||
buffer := bytes.NewBuffer(make([]byte, 0, sizeToDetectType))
|
buffer := bytes.NewBuffer(make([]byte, 0, sizeToDetectType))
|
||||||
getParams := &layer.GetObjectParams{
|
getParams := &layer.GetObjectParams{
|
||||||
ObjectInfo: inf,
|
ObjectInfo: info,
|
||||||
Writer: buffer,
|
Writer: buffer,
|
||||||
Range: getRangeToDetectContentType(inf.Size),
|
Range: getRangeToDetectContentType(info.Size),
|
||||||
VersionID: reqInfo.URL.Query().Get("versionId"),
|
VersionID: reqInfo.URL.Query().Get(api.QueryVersionID),
|
||||||
}
|
}
|
||||||
if err = h.obj.GetObject(r.Context(), getParams); err != nil {
|
if err = h.obj.GetObject(r.Context(), getParams); err != nil {
|
||||||
h.logAndSendError(w, "could not get object", reqInfo, err, zap.Stringer("oid", inf.ID()))
|
h.logAndSendError(w, "could not get object", reqInfo, err, zap.Stringer("oid", info.ID()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
inf.ContentType = http.DetectContentType(buffer.Bytes())
|
info.ContentType = http.DetectContentType(buffer.Bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
writeHeaders(w.Header(), inf)
|
writeHeaders(w.Header(), info)
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -217,6 +217,16 @@ func (h *handler) ListBucketObjectVersionsHandler(w http.ResponseWriter, r *http
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bktInfo, err := h.obj.GetBucketInfo(r.Context(), reqInfo.BucketName)
|
||||||
|
if err != nil {
|
||||||
|
h.logAndSendError(w, "could not get bucket info", reqInfo, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = checkOwner(bktInfo, r.Header.Get(api.AmzExpectedBucketOwner)); err != nil {
|
||||||
|
h.logAndSendError(w, "expected owner doesn't match", reqInfo, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
info, err := h.obj.ListObjectVersions(r.Context(), p)
|
info, err := h.obj.ListObjectVersions(r.Context(), p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logAndSendError(w, "something went wrong", reqInfo, err)
|
h.logAndSendError(w, "something went wrong", reqInfo, err)
|
||||||
|
|
|
@ -116,6 +116,12 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if versioning, err := h.obj.GetBucketVersioning(r.Context(), reqInfo.BucketName); err != nil {
|
||||||
|
h.log.Warn("couldn't get bucket versioning", zap.String("bucket name", reqInfo.BucketName), zap.Error(err))
|
||||||
|
} else if versioning.VersioningEnabled {
|
||||||
|
w.Header().Set(api.AmzVersionID, info.Version())
|
||||||
|
}
|
||||||
|
|
||||||
w.Header().Set(api.ETag, info.HashSum)
|
w.Header().Set(api.ETag, info.HashSum)
|
||||||
api.WriteSuccessResponseHeadersOnly(w)
|
api.WriteSuccessResponseHeadersOnly(w)
|
||||||
}
|
}
|
||||||
|
|
|
@ -167,7 +167,7 @@ type ListObjectsVersionsResponse struct {
|
||||||
// VersioningConfiguration contains VersioningConfiguration XML representation.
|
// VersioningConfiguration contains VersioningConfiguration XML representation.
|
||||||
type VersioningConfiguration struct {
|
type VersioningConfiguration struct {
|
||||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ VersioningConfiguration"`
|
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ VersioningConfiguration"`
|
||||||
Status string `xml:"Status"`
|
Status string `xml:"Status,omitempty"`
|
||||||
MfaDelete string `xml:"MfaDelete,omitempty"`
|
MfaDelete string `xml:"MfaDelete,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,16 @@ func (h *handler) PutBucketVersioningHandler(w http.ResponseWriter, r *http.Requ
|
||||||
Settings: &layer.BucketSettings{VersioningEnabled: configuration.Status == "Enabled"},
|
Settings: &layer.BucketSettings{VersioningEnabled: configuration.Status == "Enabled"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bktInfo, err := h.obj.GetBucketInfo(r.Context(), reqInfo.BucketName)
|
||||||
|
if err != nil {
|
||||||
|
h.logAndSendError(w, "could not get bucket info", reqInfo, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = checkOwner(bktInfo, r.Header.Get(api.AmzExpectedBucketOwner)); err != nil {
|
||||||
|
h.logAndSendError(w, "expected owner doesn't match", reqInfo, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := h.obj.PutBucketVersioning(r.Context(), p); err != nil {
|
if _, err := h.obj.PutBucketVersioning(r.Context(), p); err != nil {
|
||||||
h.logAndSendError(w, "couldn't put update versioning settings", reqInfo, err)
|
h.logAndSendError(w, "couldn't put update versioning settings", reqInfo, err)
|
||||||
}
|
}
|
||||||
|
@ -33,14 +43,27 @@ func (h *handler) PutBucketVersioningHandler(w http.ResponseWriter, r *http.Requ
|
||||||
func (h *handler) GetBucketVersioningHandler(w http.ResponseWriter, r *http.Request) {
|
func (h *handler) GetBucketVersioningHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
reqInfo := api.GetReqInfo(r.Context())
|
reqInfo := api.GetReqInfo(r.Context())
|
||||||
|
|
||||||
|
bktInfo, err := h.obj.GetBucketInfo(r.Context(), reqInfo.BucketName)
|
||||||
|
if err != nil {
|
||||||
|
h.logAndSendError(w, "could not get bucket info", reqInfo, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = checkOwner(bktInfo, r.Header.Get(api.AmzExpectedBucketOwner)); err != nil {
|
||||||
|
h.logAndSendError(w, "expected owner doesn't match", reqInfo, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
settings, err := h.obj.GetBucketVersioning(r.Context(), reqInfo.BucketName)
|
settings, err := h.obj.GetBucketVersioning(r.Context(), reqInfo.BucketName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.IsS3Error(err, errors.ErrNoSuchBucket) {
|
||||||
|
h.logAndSendError(w, "couldn't get versioning settings", reqInfo, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
h.log.Warn("couldn't get version settings object: default version settings will be used",
|
h.log.Warn("couldn't get version settings object: default version settings will be used",
|
||||||
zap.String("request_id", reqInfo.RequestID),
|
zap.String("request_id", reqInfo.RequestID),
|
||||||
zap.String("method", reqInfo.API),
|
zap.String("method", reqInfo.API),
|
||||||
zap.String("object_name", reqInfo.ObjectName),
|
zap.String("bucket_name", reqInfo.BucketName),
|
||||||
zap.Error(err))
|
zap.Error(err))
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = api.EncodeToResponse(w, formVersioningConfiguration(settings)); err != nil {
|
if err = api.EncodeToResponse(w, formVersioningConfiguration(settings)); err != nil {
|
||||||
|
@ -49,12 +72,14 @@ func (h *handler) GetBucketVersioningHandler(w http.ResponseWriter, r *http.Requ
|
||||||
}
|
}
|
||||||
|
|
||||||
func formVersioningConfiguration(settings *layer.BucketSettings) *VersioningConfiguration {
|
func formVersioningConfiguration(settings *layer.BucketSettings) *VersioningConfiguration {
|
||||||
res := &VersioningConfiguration{Status: "Suspended"}
|
res := &VersioningConfiguration{}
|
||||||
if settings == nil {
|
if settings == nil {
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
if settings.VersioningEnabled {
|
if settings.VersioningEnabled {
|
||||||
res.Status = "Enabled"
|
res.Status = "Enabled"
|
||||||
|
} else {
|
||||||
|
res.Status = "Suspended"
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,3 +44,8 @@ const (
|
||||||
|
|
||||||
ContainerID = "X-Container-Id"
|
ContainerID = "X-Container-Id"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// S3 request query params.
|
||||||
|
const (
|
||||||
|
QueryVersionID = "versionId"
|
||||||
|
)
|
||||||
|
|
|
@ -117,29 +117,42 @@ func (n *layer) containerList(ctx context.Context) ([]*cache.BucketInfo, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *layer) createContainer(ctx context.Context, p *CreateBucketParams) (*cid.ID, error) {
|
func (n *layer) createContainer(ctx context.Context, p *CreateBucketParams) (*cid.ID, error) {
|
||||||
|
var err error
|
||||||
|
bktInfo := &cache.BucketInfo{
|
||||||
|
Name: p.Name,
|
||||||
|
Owner: n.Owner(ctx),
|
||||||
|
Created: time.Now(),
|
||||||
|
BasicACL: p.ACL,
|
||||||
|
}
|
||||||
cnr := container.New(
|
cnr := container.New(
|
||||||
container.WithPolicy(p.Policy),
|
container.WithPolicy(p.Policy),
|
||||||
container.WithCustomBasicACL(p.ACL),
|
container.WithCustomBasicACL(p.ACL),
|
||||||
container.WithAttribute(container.AttributeName, p.Name),
|
container.WithAttribute(container.AttributeName, p.Name),
|
||||||
container.WithAttribute(container.AttributeTimestamp, strconv.FormatInt(time.Now().Unix(), 10)))
|
container.WithAttribute(container.AttributeTimestamp, strconv.FormatInt(bktInfo.Created.Unix(), 10)))
|
||||||
|
|
||||||
cnr.SetSessionToken(p.BoxData.Gate.SessionToken)
|
cnr.SetSessionToken(p.BoxData.Gate.SessionToken)
|
||||||
cnr.SetOwnerID(n.Owner(ctx))
|
cnr.SetOwnerID(bktInfo.Owner)
|
||||||
|
|
||||||
cid, err := n.pool.PutContainer(ctx, cnr)
|
if bktInfo.CID, err = n.pool.PutContainer(ctx, cnr); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create a bucket: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = n.pool.WaitForContainerPresence(ctx, cid, pool.DefaultPollingParams()); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := n.setContainerEACLTable(ctx, cid, p.EACL); err != nil {
|
if err = n.pool.WaitForContainerPresence(ctx, bktInfo.CID, pool.DefaultPollingParams()); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return cid, nil
|
if err = n.setContainerEACLTable(ctx, bktInfo.CID, p.EACL); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = n.bucketCache.Put(bktInfo); err != nil {
|
||||||
|
n.log.Warn("couldn't put bucket info into cache",
|
||||||
|
zap.String("bucket name", bktInfo.Name),
|
||||||
|
zap.Stringer("bucket cid", bktInfo.CID),
|
||||||
|
zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return bktInfo.CID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *layer) setContainerEACLTable(ctx context.Context, cid *cid.ID, table *eacl.Table) error {
|
func (n *layer) setContainerEACLTable(ctx context.Context, cid *cid.ID, table *eacl.Table) error {
|
||||||
|
|
|
@ -7,9 +7,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/nspcc-dev/neofs-api-go/pkg/acl/eacl"
|
"github.com/nspcc-dev/neofs-api-go/pkg/acl/eacl"
|
||||||
|
@ -32,7 +29,7 @@ type (
|
||||||
log *zap.Logger
|
log *zap.Logger
|
||||||
listsCache ObjectsListCache
|
listsCache ObjectsListCache
|
||||||
objCache cache.ObjectsCache
|
objCache cache.ObjectsCache
|
||||||
headCache cache.HeadObjectsCache
|
namesCache cache.ObjectsNameCache
|
||||||
bucketCache cache.BucketCache
|
bucketCache cache.BucketCache
|
||||||
systemCache cache.SystemCache
|
systemCache cache.SystemCache
|
||||||
}
|
}
|
||||||
|
@ -56,12 +53,10 @@ type (
|
||||||
GetObjectParams struct {
|
GetObjectParams struct {
|
||||||
Range *RangeParams
|
Range *RangeParams
|
||||||
ObjectInfo *ObjectInfo
|
ObjectInfo *ObjectInfo
|
||||||
//Bucket string
|
Offset int64
|
||||||
//Object string
|
Length int64
|
||||||
Offset int64
|
Writer io.Writer
|
||||||
Length int64
|
VersionID string
|
||||||
Writer io.Writer
|
|
||||||
VersionID string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// HeadObjectParams stores object head request parameters.
|
// HeadObjectParams stores object head request parameters.
|
||||||
|
@ -139,14 +134,6 @@ type (
|
||||||
VersionID string
|
VersionID string
|
||||||
}
|
}
|
||||||
|
|
||||||
objectVersions struct {
|
|
||||||
name string
|
|
||||||
objects []*ObjectInfo
|
|
||||||
addList []string
|
|
||||||
delList []string
|
|
||||||
isSorted bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// NeoFS provides basic NeoFS interface.
|
// NeoFS provides basic NeoFS interface.
|
||||||
NeoFS interface {
|
NeoFS interface {
|
||||||
Get(ctx context.Context, address *object.Address) (*object.Object, error)
|
Get(ctx context.Context, address *object.Address) (*object.Object, error)
|
||||||
|
@ -181,95 +168,6 @@ type (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func newObjectVersions(name string) *objectVersions {
|
|
||||||
return &objectVersions{name: name}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *objectVersions) appendVersion(oi *ObjectInfo) {
|
|
||||||
addVers := append(splitVersions(oi.Headers[versionsAddAttr]), oi.Version())
|
|
||||||
delVers := splitVersions(oi.Headers[versionsDelAttr])
|
|
||||||
v.objects = append(v.objects, oi)
|
|
||||||
for _, add := range addVers {
|
|
||||||
if !contains(v.addList, add) {
|
|
||||||
v.addList = append(v.addList, add)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 {
|
|
||||||
sortVersions(v.objects)
|
|
||||||
v.isSorted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *objectVersions) getLast() *ObjectInfo {
|
|
||||||
if len(v.objects) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
v.sort()
|
|
||||||
existedVersions := getExistedVersions(v)
|
|
||||||
for i := len(v.objects) - 1; i >= 0; i-- {
|
|
||||||
if contains(existedVersions, v.objects[i].Version()) {
|
|
||||||
delMarkHeader := v.objects[i].Headers[versionsDeleteMarkAttr]
|
|
||||||
if delMarkHeader == "" {
|
|
||||||
return v.objects[i]
|
|
||||||
}
|
|
||||||
if delMarkHeader == delMarkFullObject {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *objectVersions) getFiltered() []*ObjectInfo {
|
|
||||||
if len(v.objects) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
v.sort()
|
|
||||||
existedVersions := getExistedVersions(v)
|
|
||||||
res := make([]*ObjectInfo, 0, len(v.objects))
|
|
||||||
|
|
||||||
for _, version := range v.objects {
|
|
||||||
delMark := version.Headers[versionsDeleteMarkAttr]
|
|
||||||
if contains(existedVersions, version.Version()) && (delMark == delMarkFullObject || delMark == "") {
|
|
||||||
res = append(res, version)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *objectVersions) getAddHeader() string {
|
|
||||||
return strings.Join(v.addList, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *objectVersions) getDelHeader() string {
|
|
||||||
return strings.Join(v.delList, ",")
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
unversionedObjectVersionID = "null"
|
|
||||||
bktVersionSettingsObject = ".s3-versioning-settings"
|
|
||||||
objectSystemAttributeName = "S3-System-name"
|
|
||||||
attrVersionsIgnore = "S3-Versions-ignore"
|
|
||||||
attrSettingsVersioningEnabled = "S3-Settings-Versioning-enabled"
|
|
||||||
versionsDelAttr = "S3-Versions-del"
|
|
||||||
versionsAddAttr = "S3-Versions-add"
|
|
||||||
versionsDeleteMarkAttr = "S3-Versions-delete-mark"
|
|
||||||
delMarkFullObject = "*"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (t *VersionedObject) String() string {
|
func (t *VersionedObject) String() string {
|
||||||
return t.Name + ":" + t.VersionID
|
return t.Name + ":" + t.VersionID
|
||||||
}
|
}
|
||||||
|
@ -283,7 +181,7 @@ func NewLayer(log *zap.Logger, conns pool.Pool, config *CacheConfig) Client {
|
||||||
listsCache: newListObjectsCache(config.ListObjectsLifetime),
|
listsCache: newListObjectsCache(config.ListObjectsLifetime),
|
||||||
objCache: cache.New(config.Size, config.Lifetime),
|
objCache: cache.New(config.Size, config.Lifetime),
|
||||||
//todo reconsider cache params
|
//todo reconsider cache params
|
||||||
headCache: cache.NewHeadObject(1000, time.Minute),
|
namesCache: cache.NewObjectsNameCache(1000, time.Minute),
|
||||||
bucketCache: cache.NewBucketCache(150, time.Minute),
|
bucketCache: cache.NewBucketCache(150, time.Minute),
|
||||||
systemCache: cache.NewSystemCache(1000, 5*time.Minute),
|
systemCache: cache.NewSystemCache(1000, 5*time.Minute),
|
||||||
}
|
}
|
||||||
|
@ -432,11 +330,11 @@ func (n *layer) GetObjectInfo(ctx context.Context, p *HeadObjectParams) (*Object
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *layer) getSettingsObjectInfo(ctx context.Context, bkt *cache.BucketInfo) (*ObjectInfo, error) {
|
func (n *layer) getSettingsObjectInfo(ctx context.Context, bkt *cache.BucketInfo) (*ObjectInfo, error) {
|
||||||
if meta := n.systemCache.Get(bktVersionSettingsObject); meta != nil {
|
if meta := n.systemCache.Get(bkt.SettingsObjectKey()); meta != nil {
|
||||||
return objInfoFromMeta(bkt, meta), nil
|
return objInfoFromMeta(bkt, meta), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
oid, err := n.objectFindID(ctx, &findParams{cid: bkt.CID, attr: objectSystemAttributeName, val: bktVersionSettingsObject})
|
oid, err := n.objectFindID(ctx, &findParams{cid: bkt.CID, attr: objectSystemAttributeName, val: bkt.SettingsObjectName()})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -446,7 +344,7 @@ func (n *layer) getSettingsObjectInfo(ctx context.Context, bkt *cache.BucketInfo
|
||||||
n.log.Error("could not fetch object head", zap.Error(err))
|
n.log.Error("could not fetch object head", zap.Error(err))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err = n.systemCache.Put(bktVersionSettingsObject, meta); err != nil {
|
if err = n.systemCache.Put(bkt.SettingsObjectKey(), meta); err != nil {
|
||||||
n.log.Error("couldn't cache system object", zap.Error(err))
|
n.log.Error("couldn't cache system object", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -506,7 +404,7 @@ func (n *layer) deleteObject(ctx context.Context, bkt *cache.BucketInfo, obj *Ve
|
||||||
Header: map[string]string{versionsDeleteMarkAttr: obj.VersionID},
|
Header: map[string]string{versionsDeleteMarkAttr: obj.VersionID},
|
||||||
}
|
}
|
||||||
if len(obj.VersionID) != 0 {
|
if len(obj.VersionID) != 0 {
|
||||||
id, err := n.checkVersionsExists(ctx, bkt, obj)
|
id, err := n.checkVersionsExist(ctx, bkt, obj)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -517,41 +415,24 @@ func (n *layer) deleteObject(ctx context.Context, bkt *cache.BucketInfo, obj *Ve
|
||||||
p.Header[versionsDeleteMarkAttr] = delMarkFullObject
|
p.Header[versionsDeleteMarkAttr] = delMarkFullObject
|
||||||
}
|
}
|
||||||
if _, err = n.objectPut(ctx, bkt, p); err != nil {
|
if _, err = n.objectPut(ctx, bkt, p); err != nil {
|
||||||
return &errors.DeleteError{Err: err, Object: obj.String()}
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ids, err = n.objectSearch(ctx, &findParams{cid: bkt.CID, val: obj.Name})
|
ids, err = n.objectSearch(ctx, &findParams{cid: bkt.CID, val: obj.Name})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &errors.DeleteError{Err: err, Object: obj.String()}
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, id := range ids {
|
for _, id := range ids {
|
||||||
if err = n.objectDelete(ctx, bkt.CID, id); err != nil {
|
if err = n.objectDelete(ctx, bkt.CID, id); err != nil {
|
||||||
return &errors.DeleteError{Err: err, Object: obj.String()}
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *layer) checkVersionsExists(ctx context.Context, bkt *cache.BucketInfo, obj *VersionedObject) (*object.ID, error) {
|
|
||||||
id := object.NewID()
|
|
||||||
if err := id.Parse(obj.VersionID); err != nil {
|
|
||||||
return nil, &errors.DeleteError{Err: errors.GetAPIError(errors.ErrInvalidVersion), Object: obj.String()}
|
|
||||||
}
|
|
||||||
|
|
||||||
versions, err := n.headVersions(ctx, bkt, obj.Name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &errors.DeleteError{Err: err, Object: obj.String()}
|
|
||||||
}
|
|
||||||
if !contains(getExistedVersions(versions), obj.VersionID) {
|
|
||||||
return nil, &errors.DeleteError{Err: errors.GetAPIError(errors.ErrInvalidVersion), Object: obj.String()}
|
|
||||||
}
|
|
||||||
|
|
||||||
return id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteObjects from the storage.
|
// DeleteObjects from the storage.
|
||||||
func (n *layer) DeleteObjects(ctx context.Context, bucket string, objects []*VersionedObject) []error {
|
func (n *layer) DeleteObjects(ctx context.Context, bucket string, objects []*VersionedObject) []error {
|
||||||
var errs = make([]error, 0, len(objects))
|
var errs = make([]error, 0, len(objects))
|
||||||
|
@ -561,9 +442,9 @@ func (n *layer) DeleteObjects(ctx context.Context, bucket string, objects []*Ver
|
||||||
return append(errs, err)
|
return append(errs, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range objects {
|
for _, obj := range objects {
|
||||||
if err := n.deleteObject(ctx, bkt, objects[i]); err != nil {
|
if err := n.deleteObject(ctx, bkt, obj); err != nil {
|
||||||
errs = append(errs, err)
|
errs = append(errs, &errors.ObjectError{Err: err, Object: obj.Name, Version: obj.VersionID})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -588,210 +469,17 @@ func (n *layer) DeleteBucket(ctx context.Context, p *DeleteBucketParams) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ids, err := n.objectSearch(ctx, &findParams{cid: bucketInfo.CID})
|
objects, err := n.listSortedObjectsFromNeoFS(ctx, allObjectParams{Bucket: bucketInfo})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if len(ids) != 0 {
|
if len(objects) != 0 {
|
||||||
return errors.GetAPIError(errors.ErrBucketNotEmpty)
|
return errors.GetAPIError(errors.ErrBucketNotEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
return n.deleteContainer(ctx, bucketInfo.CID)
|
if err = n.deleteContainer(ctx, bucketInfo.CID); err != nil {
|
||||||
}
|
return err
|
||||||
|
}
|
||||||
func (n *layer) ListObjectVersions(ctx context.Context, p *ListObjectVersionsParams) (*ListObjectVersionsInfo, error) {
|
n.bucketCache.Delete(bucketInfo.Name)
|
||||||
var versions map[string]*objectVersions
|
return nil
|
||||||
res := &ListObjectVersionsInfo{}
|
|
||||||
|
|
||||||
bkt, err := n.GetBucketInfo(ctx, p.Bucket)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cacheKey, err := createKey(ctx, bkt.CID, listVersionsMethod, p.Prefix, p.Delimiter)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
allObjects := n.listsCache.Get(cacheKey)
|
|
||||||
if allObjects == nil {
|
|
||||||
versions, err = n.getAllObjectsVersions(ctx, bkt, p.Prefix, p.Delimiter)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
sortedNames := make([]string, 0, len(versions))
|
|
||||||
for k := range versions {
|
|
||||||
sortedNames = append(sortedNames, k)
|
|
||||||
}
|
|
||||||
sort.Strings(sortedNames)
|
|
||||||
|
|
||||||
allObjects = make([]*ObjectInfo, 0, p.MaxKeys)
|
|
||||||
for _, name := range sortedNames {
|
|
||||||
allObjects = append(allObjects, versions[name].getFiltered()...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// putting to cache a copy of allObjects because allObjects can be modified further
|
|
||||||
n.listsCache.Put(cacheKey, append([]*ObjectInfo(nil), allObjects...))
|
|
||||||
}
|
|
||||||
|
|
||||||
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}
|
|
||||||
if i == len(allObjects)-1 || allObjects[i+1].Name != obj.Name {
|
|
||||||
objects[i].IsLatest = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res.Version, res.DeleteMarker = triageVersions(objects)
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func sortVersions(versions []*ObjectInfo) {
|
|
||||||
sort.Slice(versions, func(i, j int) bool {
|
|
||||||
return less(versions[i], versions[j])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func triageVersions(objVersions []*ObjectVersionInfo) ([]*ObjectVersionInfo, []*ObjectVersionInfo) {
|
|
||||||
if len(objVersions) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var resVersion []*ObjectVersionInfo
|
|
||||||
var resDelMarkVersions []*ObjectVersionInfo
|
|
||||||
|
|
||||||
for _, version := range objVersions {
|
|
||||||
if version.Object.Headers[versionsDeleteMarkAttr] == delMarkFullObject {
|
|
||||||
resDelMarkVersions = append(resDelMarkVersions, version)
|
|
||||||
} else {
|
|
||||||
resVersion = append(resVersion, version)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return resVersion, resDelMarkVersions
|
|
||||||
}
|
|
||||||
|
|
||||||
func less(ov1, ov2 *ObjectInfo) bool {
|
|
||||||
if ov1.CreationEpoch == ov2.CreationEpoch {
|
|
||||||
return ov1.Version() < ov2.Version()
|
|
||||||
}
|
|
||||||
return ov1.CreationEpoch < ov2.CreationEpoch
|
|
||||||
}
|
|
||||||
|
|
||||||
func contains(list []string, elem string) bool {
|
|
||||||
for _, item := range list {
|
|
||||||
if elem == item {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *layer) PutBucketVersioning(ctx context.Context, p *PutVersioningParams) (*ObjectInfo, error) {
|
|
||||||
bucketInfo, err := n.GetBucketInfo(ctx, p.Bucket)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
objectInfo, err := n.getSettingsObjectInfo(ctx, bucketInfo)
|
|
||||||
if err != nil {
|
|
||||||
n.log.Warn("couldn't get bucket version settings object, new one will be created",
|
|
||||||
zap.String("bucket_name", bucketInfo.Name),
|
|
||||||
zap.Stringer("cid", bucketInfo.CID),
|
|
||||||
zap.Error(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
attributes := make([]*object.Attribute, 0, 3)
|
|
||||||
|
|
||||||
filename := object.NewAttribute()
|
|
||||||
filename.SetKey(objectSystemAttributeName)
|
|
||||||
filename.SetValue(bktVersionSettingsObject)
|
|
||||||
|
|
||||||
createdAt := object.NewAttribute()
|
|
||||||
createdAt.SetKey(object.AttributeTimestamp)
|
|
||||||
createdAt.SetValue(strconv.FormatInt(time.Now().UTC().Unix(), 10))
|
|
||||||
|
|
||||||
versioningIgnore := object.NewAttribute()
|
|
||||||
versioningIgnore.SetKey(attrVersionsIgnore)
|
|
||||||
versioningIgnore.SetValue(strconv.FormatBool(true))
|
|
||||||
|
|
||||||
settingsVersioningEnabled := object.NewAttribute()
|
|
||||||
settingsVersioningEnabled.SetKey(attrSettingsVersioningEnabled)
|
|
||||||
settingsVersioningEnabled.SetValue(strconv.FormatBool(p.Settings.VersioningEnabled))
|
|
||||||
|
|
||||||
attributes = append(attributes, filename, createdAt, versioningIgnore, settingsVersioningEnabled)
|
|
||||||
|
|
||||||
raw := object.NewRaw()
|
|
||||||
raw.SetOwnerID(bucketInfo.Owner)
|
|
||||||
raw.SetContainerID(bucketInfo.CID)
|
|
||||||
raw.SetAttributes(attributes...)
|
|
||||||
|
|
||||||
ops := new(client.PutObjectParams).WithObject(raw.Object())
|
|
||||||
oid, err := n.pool.PutObject(ctx, ops, n.BearerOpt(ctx))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
meta, err := n.objectHead(ctx, bucketInfo.CID, oid)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if objectInfo != nil {
|
|
||||||
if err = n.objectDelete(ctx, bucketInfo.CID, objectInfo.ID()); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return objectInfoFromMeta(bucketInfo, meta, "", ""), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *layer) GetBucketVersioning(ctx context.Context, bucketName string) (*BucketSettings, error) {
|
|
||||||
bktInfo, err := n.GetBucketInfo(ctx, bucketName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return n.getBucketSettings(ctx, bktInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *layer) getBucketSettings(ctx context.Context, bktInfo *cache.BucketInfo) (*BucketSettings, error) {
|
|
||||||
objInfo, err := n.getSettingsObjectInfo(ctx, bktInfo)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return objectInfoToBucketSettings(objInfo), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func objectInfoToBucketSettings(info *ObjectInfo) *BucketSettings {
|
|
||||||
res := &BucketSettings{}
|
|
||||||
|
|
||||||
enabled, ok := info.Headers["S3-Settings-Versioning-enabled"]
|
|
||||||
if ok {
|
|
||||||
if parsed, err := strconv.ParseBool(enabled); err == nil {
|
|
||||||
res.VersioningEnabled = parsed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -144,7 +144,7 @@ func (n *layer) objectPut(ctx context.Context, bkt *cache.BucketInfo, p *PutObje
|
||||||
|
|
||||||
r := p.Reader
|
r := p.Reader
|
||||||
if len(p.Header[api.ContentType]) == 0 {
|
if len(p.Header[api.ContentType]) == 0 {
|
||||||
d := newDetector(p.Reader)
|
d := newDetector(r)
|
||||||
if contentType, err := d.Detect(); err == nil {
|
if contentType, err := d.Detect(); err == nil {
|
||||||
p.Header[api.ContentType] = contentType
|
p.Header[api.ContentType] = contentType
|
||||||
}
|
}
|
||||||
|
@ -271,7 +271,7 @@ func updateCRDT2PSetHeaders(p *PutObjectParams, versions *objectVersions, versio
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *layer) headLastVersionIfNotDeleted(ctx context.Context, bkt *cache.BucketInfo, objectName string) (*ObjectInfo, error) {
|
func (n *layer) headLastVersionIfNotDeleted(ctx context.Context, bkt *cache.BucketInfo, objectName string) (*ObjectInfo, error) {
|
||||||
if address := n.headCache.Get(bkt.Name + "/" + objectName); address != nil {
|
if address := n.namesCache.Get(bkt.Name + "/" + objectName); address != nil {
|
||||||
if headInfo := n.objCache.Get(address); headInfo != nil {
|
if headInfo := n.objCache.Get(address); headInfo != nil {
|
||||||
return objInfoFromMeta(bkt, headInfo), nil
|
return objInfoFromMeta(bkt, headInfo), nil
|
||||||
}
|
}
|
||||||
|
@ -287,7 +287,7 @@ func (n *layer) headLastVersionIfNotDeleted(ctx context.Context, bkt *cache.Buck
|
||||||
return nil, apiErrors.GetAPIError(apiErrors.ErrNoSuchKey)
|
return nil, apiErrors.GetAPIError(apiErrors.ErrNoSuchKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = n.headCache.Put(lastVersion.NiceName(), lastVersion.Address()); err != nil {
|
if err = n.namesCache.Put(lastVersion.NiceName(), lastVersion.Address()); err != nil {
|
||||||
n.log.Warn("couldn't put obj address to head cache",
|
n.log.Warn("couldn't put obj address to head cache",
|
||||||
zap.String("obj nice name", lastVersion.NiceName()),
|
zap.String("obj nice name", lastVersion.NiceName()),
|
||||||
zap.Error(err))
|
zap.Error(err))
|
||||||
|
@ -487,14 +487,12 @@ func (n *layer) getAllObjectsVersions(ctx context.Context, bkt *cache.BucketInfo
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if objVersions, ok := versions[oi.Name]; ok {
|
objVersions, ok := versions[oi.Name]
|
||||||
objVersions.appendVersion(oi)
|
if !ok {
|
||||||
versions[oi.Name] = objVersions
|
objVersions = newObjectVersions(oi.Name)
|
||||||
} else {
|
|
||||||
objVersion := newObjectVersions(oi.Name)
|
|
||||||
objVersion.appendVersion(oi)
|
|
||||||
versions[oi.Name] = objVersion
|
|
||||||
}
|
}
|
||||||
|
objVersions.appendVersion(oi)
|
||||||
|
versions[oi.Name] = objVersions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,7 @@ type (
|
||||||
list []*ObjectInfo
|
list []*ObjectInfo
|
||||||
}
|
}
|
||||||
cacheOptions struct {
|
cacheOptions struct {
|
||||||
|
method string
|
||||||
key string
|
key string
|
||||||
delimiter string
|
delimiter string
|
||||||
prefix string
|
prefix string
|
||||||
|
@ -89,6 +90,7 @@ func createKey(ctx context.Context, cid *cid.ID, method, prefix, delimiter strin
|
||||||
return cacheOptions{}, err
|
return cacheOptions{}, err
|
||||||
}
|
}
|
||||||
p := cacheOptions{
|
p := cacheOptions{
|
||||||
|
method: method,
|
||||||
key: box.Gate.AccessKey + cid.String(),
|
key: box.Gate.AccessKey + cid.String(),
|
||||||
delimiter: delimiter,
|
delimiter: delimiter,
|
||||||
prefix: prefix,
|
prefix: prefix,
|
||||||
|
|
325
api/layer/versioning.go
Normal file
325
api/layer/versioning.go
Normal file
|
@ -0,0 +1,325 @@
|
||||||
|
package layer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neofs-api-go/pkg/client"
|
||||||
|
"github.com/nspcc-dev/neofs-api-go/pkg/object"
|
||||||
|
"github.com/nspcc-dev/neofs-s3-gw/api/cache"
|
||||||
|
"github.com/nspcc-dev/neofs-s3-gw/api/errors"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type objectVersions struct {
|
||||||
|
name string
|
||||||
|
objects []*ObjectInfo
|
||||||
|
addList []string
|
||||||
|
delList []string
|
||||||
|
isSorted bool
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
unversionedObjectVersionID = "null"
|
||||||
|
objectSystemAttributeName = "S3-System-name"
|
||||||
|
attrVersionsIgnore = "S3-Versions-ignore"
|
||||||
|
attrSettingsVersioningEnabled = "S3-Settings-Versioning-enabled"
|
||||||
|
versionsDelAttr = "S3-Versions-del"
|
||||||
|
versionsAddAttr = "S3-Versions-add"
|
||||||
|
versionsDeleteMarkAttr = "S3-Versions-delete-mark"
|
||||||
|
delMarkFullObject = "*"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newObjectVersions(name string) *objectVersions {
|
||||||
|
return &objectVersions{name: name}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *objectVersions) appendVersion(oi *ObjectInfo) {
|
||||||
|
addVers := append(splitVersions(oi.Headers[versionsAddAttr]), oi.Version())
|
||||||
|
delVers := splitVersions(oi.Headers[versionsDelAttr])
|
||||||
|
v.objects = append(v.objects, oi)
|
||||||
|
for _, add := range addVers {
|
||||||
|
if !contains(v.addList, add) {
|
||||||
|
v.addList = append(v.addList, add)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
return less(v.objects[i], v.objects[j])
|
||||||
|
})
|
||||||
|
v.isSorted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *objectVersions) getLast() *ObjectInfo {
|
||||||
|
if len(v.objects) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
v.sort()
|
||||||
|
existedVersions := getExistedVersions(v)
|
||||||
|
for i := len(v.objects) - 1; i >= 0; i-- {
|
||||||
|
if contains(existedVersions, v.objects[i].Version()) {
|
||||||
|
delMarkHeader := v.objects[i].Headers[versionsDeleteMarkAttr]
|
||||||
|
if delMarkHeader == "" {
|
||||||
|
return v.objects[i]
|
||||||
|
}
|
||||||
|
if delMarkHeader == delMarkFullObject {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *objectVersions) getFiltered() []*ObjectInfo {
|
||||||
|
if len(v.objects) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
v.sort()
|
||||||
|
existedVersions := getExistedVersions(v)
|
||||||
|
res := make([]*ObjectInfo, 0, len(v.objects))
|
||||||
|
|
||||||
|
for _, version := range v.objects {
|
||||||
|
delMark := version.Headers[versionsDeleteMarkAttr]
|
||||||
|
if contains(existedVersions, version.Version()) && (delMark == delMarkFullObject || delMark == "") {
|
||||||
|
res = append(res, version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *objectVersions) getAddHeader() string {
|
||||||
|
return strings.Join(v.addList, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *objectVersions) getDelHeader() string {
|
||||||
|
return strings.Join(v.delList, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *layer) PutBucketVersioning(ctx context.Context, p *PutVersioningParams) (*ObjectInfo, error) {
|
||||||
|
bucketInfo, err := n.GetBucketInfo(ctx, p.Bucket)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
objectInfo, err := n.getSettingsObjectInfo(ctx, bucketInfo)
|
||||||
|
if err != nil {
|
||||||
|
n.log.Warn("couldn't get bucket version settings object, new one will be created",
|
||||||
|
zap.String("bucket_name", bucketInfo.Name),
|
||||||
|
zap.Stringer("cid", bucketInfo.CID),
|
||||||
|
zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
attributes := make([]*object.Attribute, 0, 3)
|
||||||
|
|
||||||
|
filename := object.NewAttribute()
|
||||||
|
filename.SetKey(objectSystemAttributeName)
|
||||||
|
filename.SetValue(bucketInfo.SettingsObjectName())
|
||||||
|
|
||||||
|
createdAt := object.NewAttribute()
|
||||||
|
createdAt.SetKey(object.AttributeTimestamp)
|
||||||
|
createdAt.SetValue(strconv.FormatInt(time.Now().UTC().Unix(), 10))
|
||||||
|
|
||||||
|
versioningIgnore := object.NewAttribute()
|
||||||
|
versioningIgnore.SetKey(attrVersionsIgnore)
|
||||||
|
versioningIgnore.SetValue(strconv.FormatBool(true))
|
||||||
|
|
||||||
|
settingsVersioningEnabled := object.NewAttribute()
|
||||||
|
settingsVersioningEnabled.SetKey(attrSettingsVersioningEnabled)
|
||||||
|
settingsVersioningEnabled.SetValue(strconv.FormatBool(p.Settings.VersioningEnabled))
|
||||||
|
|
||||||
|
attributes = append(attributes, filename, createdAt, versioningIgnore, settingsVersioningEnabled)
|
||||||
|
|
||||||
|
raw := object.NewRaw()
|
||||||
|
raw.SetOwnerID(bucketInfo.Owner)
|
||||||
|
raw.SetContainerID(bucketInfo.CID)
|
||||||
|
raw.SetAttributes(attributes...)
|
||||||
|
|
||||||
|
ops := new(client.PutObjectParams).WithObject(raw.Object())
|
||||||
|
oid, err := n.pool.PutObject(ctx, ops, n.BearerOpt(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
meta, err := n.objectHead(ctx, bucketInfo.CID, oid)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = n.systemCache.Put(bucketInfo.SettingsObjectKey(), meta); err != nil {
|
||||||
|
n.log.Error("couldn't cache system object", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if objectInfo != nil {
|
||||||
|
if err = n.objectDelete(ctx, bucketInfo.CID, objectInfo.ID()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return objectInfoFromMeta(bucketInfo, meta, "", ""), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *layer) GetBucketVersioning(ctx context.Context, bucketName string) (*BucketSettings, error) {
|
||||||
|
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) {
|
||||||
|
var versions map[string]*objectVersions
|
||||||
|
res := &ListObjectVersionsInfo{}
|
||||||
|
|
||||||
|
bkt, err := n.GetBucketInfo(ctx, p.Bucket)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheKey, err := createKey(ctx, bkt.CID, listVersionsMethod, p.Prefix, p.Delimiter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
allObjects := n.listsCache.Get(cacheKey)
|
||||||
|
if allObjects == nil {
|
||||||
|
versions, err = n.getAllObjectsVersions(ctx, bkt, p.Prefix, p.Delimiter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sortedNames := make([]string, 0, len(versions))
|
||||||
|
for k := range versions {
|
||||||
|
sortedNames = append(sortedNames, k)
|
||||||
|
}
|
||||||
|
sort.Strings(sortedNames)
|
||||||
|
|
||||||
|
allObjects = make([]*ObjectInfo, 0, p.MaxKeys)
|
||||||
|
for _, name := range sortedNames {
|
||||||
|
allObjects = append(allObjects, versions[name].getFiltered()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// putting to cache a copy of allObjects because allObjects can be modified further
|
||||||
|
n.listsCache.Put(cacheKey, append([]*ObjectInfo(nil), allObjects...))
|
||||||
|
}
|
||||||
|
|
||||||
|
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}
|
||||||
|
if i == len(allObjects)-1 || allObjects[i+1].Name != obj.Name {
|
||||||
|
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 {
|
||||||
|
if version.Object.Headers[versionsDeleteMarkAttr] == delMarkFullObject {
|
||||||
|
resDelMarkVersions = append(resDelMarkVersions, version)
|
||||||
|
} else {
|
||||||
|
resVersion = append(resVersion, version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resVersion, resDelMarkVersions
|
||||||
|
}
|
||||||
|
|
||||||
|
func less(ov1, ov2 *ObjectInfo) bool {
|
||||||
|
if ov1.CreationEpoch == ov2.CreationEpoch {
|
||||||
|
return ov1.Version() < ov2.Version()
|
||||||
|
}
|
||||||
|
return ov1.CreationEpoch < ov2.CreationEpoch
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(list []string, elem string) bool {
|
||||||
|
for _, item := range list {
|
||||||
|
if elem == item {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *layer) getBucketSettings(ctx context.Context, bktInfo *cache.BucketInfo) (*BucketSettings, error) {
|
||||||
|
objInfo, err := n.getSettingsObjectInfo(ctx, bktInfo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return objectInfoToBucketSettings(objInfo), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func objectInfoToBucketSettings(info *ObjectInfo) *BucketSettings {
|
||||||
|
res := &BucketSettings{}
|
||||||
|
|
||||||
|
enabled, ok := info.Headers[attrSettingsVersioningEnabled]
|
||||||
|
if ok {
|
||||||
|
if parsed, err := strconv.ParseBool(enabled); err == nil {
|
||||||
|
res.VersioningEnabled = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *layer) checkVersionsExist(ctx context.Context, bkt *cache.BucketInfo, obj *VersionedObject) (*object.ID, error) {
|
||||||
|
id := object.NewID()
|
||||||
|
if err := id.Parse(obj.VersionID); err != nil {
|
||||||
|
return nil, errors.GetAPIError(errors.ErrInvalidVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
versions, err := n.headVersions(ctx, bkt, obj.Name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !contains(getExistedVersions(versions), obj.VersionID) {
|
||||||
|
return nil, errors.GetAPIError(errors.ErrInvalidVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
return id, nil
|
||||||
|
}
|
|
@ -226,8 +226,8 @@ See also `GetObject` and other method parameters.
|
||||||
|
|
||||||
| | Method | Comments |
|
| | Method | Comments |
|
||||||
|----|---------------------|----------|
|
|----|---------------------|----------|
|
||||||
| 🔴 | GetBucketVersioning | |
|
| 🟢 | GetBucketVersioning | |
|
||||||
| 🔴 | PutBucketVersioning | |
|
| 🟢 | PutBucketVersioning | |
|
||||||
|
|
||||||
## Website
|
## Website
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue