2024-08-12 11:22:56 +00:00
|
|
|
package handler
|
|
|
|
|
|
|
|
import (
|
2024-10-15 11:18:38 +00:00
|
|
|
"bytes"
|
2024-10-14 14:03:53 +00:00
|
|
|
"context"
|
2024-10-15 11:18:38 +00:00
|
|
|
"crypto/md5"
|
2024-10-14 14:03:53 +00:00
|
|
|
"encoding/base64"
|
2024-08-12 11:22:56 +00:00
|
|
|
"fmt"
|
2024-10-15 11:18:38 +00:00
|
|
|
"io"
|
2024-08-12 11:22:56 +00:00
|
|
|
"net/http"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
|
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
|
2024-09-27 09:18:41 +00:00
|
|
|
apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
2024-08-12 11:22:56 +00:00
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
|
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
|
2024-10-14 14:03:53 +00:00
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/util"
|
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
2024-08-12 11:22:56 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
maxRules = 1000
|
|
|
|
maxRuleIDLen = 255
|
|
|
|
maxNewerNoncurrentVersions = 100
|
|
|
|
)
|
|
|
|
|
|
|
|
func (h *handler) GetBucketLifecycleHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
ctx := r.Context()
|
|
|
|
reqInfo := middleware.GetReqInfo(ctx)
|
|
|
|
|
|
|
|
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
|
|
|
|
if err != nil {
|
2024-10-25 01:36:18 +00:00
|
|
|
h.logAndSendError(ctx, w, "could not get bucket info", reqInfo, err)
|
2024-08-12 11:22:56 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
cfg, err := h.obj.GetBucketLifecycleConfiguration(ctx, bktInfo)
|
|
|
|
if err != nil {
|
2024-10-25 01:36:18 +00:00
|
|
|
h.logAndSendError(ctx, w, "could not get bucket lifecycle configuration", reqInfo, err)
|
2024-08-12 11:22:56 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = middleware.EncodeToResponse(w, cfg); err != nil {
|
2024-10-25 01:36:18 +00:00
|
|
|
h.logAndSendError(ctx, w, "could not encode GetBucketLifecycle response", reqInfo, err)
|
2024-08-12 11:22:56 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *handler) PutBucketLifecycleHandler(w http.ResponseWriter, r *http.Request) {
|
2024-10-15 11:18:38 +00:00
|
|
|
var buf bytes.Buffer
|
|
|
|
|
|
|
|
tee := io.TeeReader(r.Body, &buf)
|
2024-08-12 11:22:56 +00:00
|
|
|
ctx := r.Context()
|
|
|
|
reqInfo := middleware.GetReqInfo(ctx)
|
|
|
|
|
|
|
|
// Content-Md5 is required and should be set
|
|
|
|
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketLifecycleConfiguration.html
|
|
|
|
if _, ok := r.Header[api.ContentMD5]; !ok {
|
2024-10-25 01:36:18 +00:00
|
|
|
h.logAndSendError(ctx, w, "missing Content-MD5", reqInfo, apierr.GetAPIError(apierr.ErrMissingContentMD5))
|
2024-08-12 11:22:56 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-10-15 11:18:38 +00:00
|
|
|
headerMD5, err := base64.StdEncoding.DecodeString(r.Header.Get(api.ContentMD5))
|
|
|
|
if err != nil {
|
2024-10-25 01:36:18 +00:00
|
|
|
h.logAndSendError(ctx, w, "invalid Content-MD5", reqInfo, apierr.GetAPIError(apierr.ErrInvalidDigest))
|
2024-10-14 14:03:53 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-10-15 11:18:38 +00:00
|
|
|
cfg := new(data.LifecycleConfiguration)
|
|
|
|
if err = h.cfg.NewXMLDecoder(tee).Decode(cfg); err != nil {
|
2024-10-25 01:36:18 +00:00
|
|
|
h.logAndSendError(ctx, w, "could not decode body", reqInfo, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrMalformedXML), err.Error()))
|
2024-10-15 11:18:38 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
bodyMD5, err := getContentMD5(&buf)
|
2024-08-12 11:22:56 +00:00
|
|
|
if err != nil {
|
2024-10-25 01:36:18 +00:00
|
|
|
h.logAndSendError(ctx, w, "could not get content md5", reqInfo, err)
|
2024-08-12 11:22:56 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-10-15 11:18:38 +00:00
|
|
|
if !bytes.Equal(headerMD5, bodyMD5) {
|
2024-10-25 01:36:18 +00:00
|
|
|
h.logAndSendError(ctx, w, "Content-MD5 does not match", reqInfo, apierr.GetAPIError(apierr.ErrInvalidDigest))
|
2024-10-15 11:18:38 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
|
|
|
|
if err != nil {
|
2024-10-25 01:36:18 +00:00
|
|
|
h.logAndSendError(ctx, w, "could not get bucket info", reqInfo, err)
|
2024-08-12 11:22:56 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-10-14 14:03:53 +00:00
|
|
|
networkInfo, err := h.obj.GetNetworkInfo(ctx)
|
|
|
|
if err != nil {
|
2024-10-25 01:36:18 +00:00
|
|
|
h.logAndSendError(ctx, w, "could not get network info", reqInfo, err)
|
2024-10-14 14:03:53 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = checkLifecycleConfiguration(ctx, cfg, &networkInfo); err != nil {
|
2024-10-25 01:36:18 +00:00
|
|
|
h.logAndSendError(ctx, w, "invalid lifecycle configuration", reqInfo, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrMalformedXML), err.Error()))
|
2024-08-12 11:22:56 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
params := &layer.PutBucketLifecycleParams{
|
2024-10-14 14:03:53 +00:00
|
|
|
BktInfo: bktInfo,
|
|
|
|
LifecycleCfg: cfg,
|
2024-08-12 11:22:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
params.CopiesNumbers, err = h.pickCopiesNumbers(parseMetadata(r), reqInfo.Namespace, bktInfo.LocationConstraint)
|
|
|
|
if err != nil {
|
2024-10-25 01:36:18 +00:00
|
|
|
h.logAndSendError(ctx, w, "invalid copies number", reqInfo, err)
|
2024-08-12 11:22:56 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = h.obj.PutBucketLifecycleConfiguration(ctx, params); err != nil {
|
2024-10-25 01:36:18 +00:00
|
|
|
h.logAndSendError(ctx, w, "could not put bucket lifecycle configuration", reqInfo, err)
|
2024-08-12 11:22:56 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (h *handler) DeleteBucketLifecycleHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
ctx := r.Context()
|
|
|
|
reqInfo := middleware.GetReqInfo(ctx)
|
|
|
|
|
|
|
|
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
|
|
|
|
if err != nil {
|
2024-10-25 01:36:18 +00:00
|
|
|
h.logAndSendError(ctx, w, "could not get bucket info", reqInfo, err)
|
2024-08-12 11:22:56 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = h.obj.DeleteBucketLifecycleConfiguration(ctx, bktInfo); err != nil {
|
2024-10-25 01:36:18 +00:00
|
|
|
h.logAndSendError(ctx, w, "could not delete bucket lifecycle configuration", reqInfo, err)
|
2024-08-12 11:22:56 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
|
|
}
|
|
|
|
|
2024-10-14 14:03:53 +00:00
|
|
|
func checkLifecycleConfiguration(ctx context.Context, cfg *data.LifecycleConfiguration, ni *netmap.NetworkInfo) error {
|
|
|
|
now := layer.TimeNow(ctx)
|
|
|
|
|
2024-08-12 11:22:56 +00:00
|
|
|
if len(cfg.Rules) > maxRules {
|
|
|
|
return fmt.Errorf("number of rules cannot be greater than %d", maxRules)
|
|
|
|
}
|
|
|
|
|
|
|
|
ids := make(map[string]struct{}, len(cfg.Rules))
|
2024-10-14 14:03:53 +00:00
|
|
|
for i, rule := range cfg.Rules {
|
2024-08-12 11:22:56 +00:00
|
|
|
if _, ok := ids[rule.ID]; ok && rule.ID != "" {
|
|
|
|
return fmt.Errorf("duplicate 'ID': %s", rule.ID)
|
|
|
|
}
|
|
|
|
ids[rule.ID] = struct{}{}
|
|
|
|
|
|
|
|
if len(rule.ID) > maxRuleIDLen {
|
|
|
|
return fmt.Errorf("'ID' value cannot be longer than %d characters", maxRuleIDLen)
|
|
|
|
}
|
|
|
|
|
|
|
|
if rule.Status != data.LifecycleStatusEnabled && rule.Status != data.LifecycleStatusDisabled {
|
|
|
|
return fmt.Errorf("invalid lifecycle status: %s", rule.Status)
|
|
|
|
}
|
|
|
|
|
|
|
|
if rule.AbortIncompleteMultipartUpload == nil && rule.Expiration == nil && rule.NonCurrentVersionExpiration == nil {
|
|
|
|
return fmt.Errorf("at least one action needs to be specified in a rule")
|
|
|
|
}
|
|
|
|
|
|
|
|
if rule.AbortIncompleteMultipartUpload != nil {
|
|
|
|
if rule.AbortIncompleteMultipartUpload.DaysAfterInitiation != nil &&
|
|
|
|
*rule.AbortIncompleteMultipartUpload.DaysAfterInitiation <= 0 {
|
|
|
|
return fmt.Errorf("days after initiation must be a positive integer: %d", *rule.AbortIncompleteMultipartUpload.DaysAfterInitiation)
|
|
|
|
}
|
|
|
|
|
|
|
|
if rule.Filter != nil && (rule.Filter.Tag != nil || (rule.Filter.And != nil && len(rule.Filter.And.Tags) > 0)) {
|
|
|
|
return fmt.Errorf("abort incomplete multipart upload cannot be specified with tags")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if rule.Expiration != nil {
|
|
|
|
if rule.Expiration.ExpiredObjectDeleteMarker != nil {
|
|
|
|
if rule.Expiration.Days != nil || rule.Expiration.Date != "" {
|
|
|
|
return fmt.Errorf("expired object delete marker cannot be specified with days or date")
|
|
|
|
}
|
|
|
|
|
|
|
|
if rule.Filter != nil && (rule.Filter.Tag != nil || (rule.Filter.And != nil && len(rule.Filter.And.Tags) > 0)) {
|
|
|
|
return fmt.Errorf("expired object delete marker cannot be specified with tags")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if rule.Expiration.Days != nil && *rule.Expiration.Days <= 0 {
|
|
|
|
return fmt.Errorf("expiration days must be a positive integer: %d", *rule.Expiration.Days)
|
|
|
|
}
|
|
|
|
|
2024-10-14 14:03:53 +00:00
|
|
|
if rule.Expiration.Date != "" {
|
|
|
|
parsedTime, err := time.Parse("2006-01-02T15:04:05Z", rule.Expiration.Date)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("invalid value of expiration date: %s", rule.Expiration.Date)
|
|
|
|
}
|
|
|
|
|
|
|
|
epoch, err := util.TimeToEpoch(ni, now, parsedTime)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("convert time to epoch: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
cfg.Rules[i].Expiration.Epoch = &epoch
|
2024-08-12 11:22:56 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if rule.NonCurrentVersionExpiration != nil {
|
|
|
|
if rule.NonCurrentVersionExpiration.NewerNonCurrentVersions != nil &&
|
|
|
|
(*rule.NonCurrentVersionExpiration.NewerNonCurrentVersions > maxNewerNoncurrentVersions ||
|
|
|
|
*rule.NonCurrentVersionExpiration.NewerNonCurrentVersions <= 0) {
|
|
|
|
return fmt.Errorf("invalid value of newer noncurrent versions: %d", *rule.NonCurrentVersionExpiration.NewerNonCurrentVersions)
|
|
|
|
}
|
|
|
|
|
|
|
|
if rule.NonCurrentVersionExpiration.NonCurrentDays != nil && *rule.NonCurrentVersionExpiration.NonCurrentDays <= 0 {
|
|
|
|
return fmt.Errorf("invalid value of noncurrent days: %d", *rule.NonCurrentVersionExpiration.NonCurrentDays)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := checkLifecycleRuleFilter(rule.Filter); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func checkLifecycleRuleFilter(filter *data.LifecycleRuleFilter) error {
|
|
|
|
if filter == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var fields int
|
|
|
|
|
|
|
|
if filter.And != nil {
|
|
|
|
fields++
|
|
|
|
for _, tag := range filter.And.Tags {
|
|
|
|
err := checkTag(tag)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if filter.And.ObjectSizeGreaterThan != nil && filter.And.ObjectSizeLessThan != nil &&
|
|
|
|
*filter.And.ObjectSizeLessThan <= *filter.And.ObjectSizeGreaterThan {
|
|
|
|
return fmt.Errorf("the maximum object size must be larger than the minimum object size")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if filter.ObjectSizeGreaterThan != nil {
|
|
|
|
fields++
|
|
|
|
}
|
|
|
|
|
|
|
|
if filter.ObjectSizeLessThan != nil {
|
|
|
|
fields++
|
|
|
|
}
|
|
|
|
|
|
|
|
if filter.Prefix != "" {
|
|
|
|
fields++
|
|
|
|
}
|
|
|
|
|
|
|
|
if filter.Tag != nil {
|
|
|
|
fields++
|
|
|
|
err := checkTag(*filter.Tag)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if fields > 1 {
|
|
|
|
return fmt.Errorf("filter cannot have more than one field")
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2024-10-15 11:18:38 +00:00
|
|
|
|
|
|
|
func getContentMD5(reader io.Reader) ([]byte, error) {
|
|
|
|
hash := md5.New()
|
|
|
|
_, err := io.Copy(hash, reader)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return hash.Sum(nil), nil
|
|
|
|
}
|