mirror of
https://github.com/versity/versitygw.git
synced 2026-04-16 02:46:59 +00:00
Fixes #998 Closes #1125 Closes #1126 Closes #1127 Implements objects meta properties(Content-Disposition, Content-Language, Content-Encoding, Cache-Control, Expires) and tagging besed on the directives(metadata, tagging) in CopyObject in posix and azure backends. The properties/tagging should be coppied from the source object if "COPY" directive is provided and it should be replaced otherwise. Changes the object copy principle in azure: instead of using the `CopyFromURL` method from azure sdk, it first loads the object then creates one, to be able to compare and store the meta properties.
272 lines
6.5 KiB
Go
272 lines
6.5 KiB
Go
// Copyright 2023 Versity Software
|
|
// This file is licensed under the Apache License, Version 2.0
|
|
// (the "License"); you may not use this file except in compliance
|
|
// with the License. You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing,
|
|
// software distributed under the License is distributed on an
|
|
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
// KIND, either express or implied. See the License for the
|
|
// specific language governing permissions and limitations
|
|
// under the License.
|
|
|
|
package backend
|
|
|
|
import (
|
|
"crypto/md5"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
|
"github.com/versity/versitygw/s3err"
|
|
"github.com/versity/versitygw/s3response"
|
|
)
|
|
|
|
const (
|
|
// this is the media type for directories in AWS and Nextcloud
|
|
DirContentType = "application/x-directory"
|
|
DefaultContentType = "binary/octet-stream"
|
|
|
|
// this is the minimum allowed size for mp parts
|
|
MinPartSize = 5 * 1024 * 1024
|
|
)
|
|
|
|
func IsValidBucketName(name string) bool { return true }
|
|
|
|
type ByBucketName []s3response.ListAllMyBucketsEntry
|
|
|
|
func (d ByBucketName) Len() int { return len(d) }
|
|
func (d ByBucketName) Swap(i, j int) { d[i], d[j] = d[j], d[i] }
|
|
func (d ByBucketName) Less(i, j int) bool { return d[i].Name < d[j].Name }
|
|
|
|
type ByObjectName []types.Object
|
|
|
|
func (d ByObjectName) Len() int { return len(d) }
|
|
func (d ByObjectName) Swap(i, j int) { d[i], d[j] = d[j], d[i] }
|
|
func (d ByObjectName) Less(i, j int) bool { return *d[i].Key < *d[j].Key }
|
|
|
|
func GetPtrFromString(str string) *string {
|
|
if str == "" {
|
|
return nil
|
|
}
|
|
return &str
|
|
}
|
|
|
|
func GetStringFromPtr(str *string) string {
|
|
if str == nil {
|
|
return ""
|
|
}
|
|
return *str
|
|
}
|
|
|
|
func GetTimePtr(t time.Time) *time.Time {
|
|
return &t
|
|
}
|
|
|
|
func TrimEtag(etag *string) *string {
|
|
if etag == nil {
|
|
return nil
|
|
}
|
|
|
|
return GetPtrFromString(strings.Trim(*etag, "\""))
|
|
}
|
|
|
|
var (
|
|
errInvalidRange = s3err.GetAPIError(s3err.ErrInvalidRange)
|
|
errInvalidCopySourceRange = s3err.GetAPIError(s3err.ErrInvalidCopySourceRange)
|
|
)
|
|
|
|
// ParseGetObjectRange parses input range header and returns startoffset, length, isValid
|
|
// and error. If no endoffset specified, then length is set to the object size
|
|
// for invalid inputs, it returns no error, but isValid=false
|
|
// `InvalidRange` error is returnd, only if startoffset is greater than the object size
|
|
func ParseGetObjectRange(size int64, acceptRange string) (int64, int64, bool, error) {
|
|
if acceptRange == "" {
|
|
return 0, size, false, nil
|
|
}
|
|
|
|
rangeKv := strings.Split(acceptRange, "=")
|
|
|
|
if len(rangeKv) != 2 {
|
|
return 0, size, false, nil
|
|
}
|
|
|
|
if rangeKv[0] != "bytes" {
|
|
return 0, size, false, nil
|
|
}
|
|
|
|
bRange := strings.Split(rangeKv[1], "-")
|
|
if len(bRange) != 2 {
|
|
return 0, size, false, nil
|
|
}
|
|
|
|
startOffset, err := strconv.ParseInt(bRange[0], 10, 64)
|
|
if err != nil {
|
|
return 0, size, false, nil
|
|
}
|
|
|
|
if startOffset >= size {
|
|
return 0, 0, false, errInvalidRange
|
|
}
|
|
|
|
if bRange[1] == "" {
|
|
return startOffset, size - startOffset, true, nil
|
|
}
|
|
|
|
endOffset, err := strconv.ParseInt(bRange[1], 10, 64)
|
|
if err != nil {
|
|
return 0, size, false, nil
|
|
}
|
|
|
|
if endOffset < startOffset {
|
|
return 0, size, false, nil
|
|
}
|
|
|
|
if endOffset >= size {
|
|
return startOffset, size - startOffset, true, nil
|
|
}
|
|
|
|
return startOffset, endOffset - startOffset + 1, true, nil
|
|
}
|
|
|
|
// ParseCopySourceRange parses input range header and returns startoffset, length
|
|
// and error. If no endoffset specified, then length is set to the object size
|
|
func ParseCopySourceRange(size int64, acceptRange string) (int64, int64, error) {
|
|
if acceptRange == "" {
|
|
return 0, size, nil
|
|
}
|
|
|
|
rangeKv := strings.Split(acceptRange, "=")
|
|
|
|
if len(rangeKv) != 2 {
|
|
return 0, 0, errInvalidCopySourceRange
|
|
}
|
|
|
|
if rangeKv[0] != "bytes" {
|
|
return 0, 0, errInvalidCopySourceRange
|
|
}
|
|
|
|
bRange := strings.Split(rangeKv[1], "-")
|
|
if len(bRange) != 2 {
|
|
return 0, 0, errInvalidCopySourceRange
|
|
}
|
|
|
|
startOffset, err := strconv.ParseInt(bRange[0], 10, 64)
|
|
if err != nil {
|
|
return 0, 0, errInvalidCopySourceRange
|
|
}
|
|
|
|
if startOffset >= size {
|
|
return 0, 0, s3err.CreateExceedingRangeErr(size)
|
|
}
|
|
|
|
if bRange[1] == "" {
|
|
return startOffset, size - startOffset + 1, nil
|
|
}
|
|
|
|
endOffset, err := strconv.ParseInt(bRange[1], 10, 64)
|
|
if err != nil {
|
|
return 0, 0, errInvalidCopySourceRange
|
|
}
|
|
|
|
if endOffset < startOffset {
|
|
return 0, 0, errInvalidCopySourceRange
|
|
}
|
|
|
|
if endOffset >= size {
|
|
return 0, 0, s3err.CreateExceedingRangeErr(size)
|
|
}
|
|
|
|
return startOffset, endOffset - startOffset + 1, nil
|
|
}
|
|
|
|
// ParseCopySource parses x-amz-copy-source header and returns source bucket,
|
|
// source object, versionId, error respectively
|
|
func ParseCopySource(copySourceHeader string) (string, string, string, error) {
|
|
if copySourceHeader[0] == '/' {
|
|
copySourceHeader = copySourceHeader[1:]
|
|
}
|
|
|
|
var copySource, versionId string
|
|
i := strings.LastIndex(copySourceHeader, "?versionId=")
|
|
if i == -1 {
|
|
copySource = copySourceHeader
|
|
} else {
|
|
copySource = copySourceHeader[:i]
|
|
versionId = copySourceHeader[i+11:]
|
|
}
|
|
|
|
srcBucket, srcObject, ok := strings.Cut(copySource, "/")
|
|
if !ok {
|
|
return "", "", "", s3err.GetAPIError(s3err.ErrInvalidCopySource)
|
|
}
|
|
|
|
return srcBucket, srcObject, versionId, nil
|
|
}
|
|
|
|
// ParseObjectTags parses the url encoded input string into
|
|
// map[string]string key-value tag set
|
|
func ParseObjectTags(t string) (map[string]string, error) {
|
|
tagging := make(map[string]string)
|
|
|
|
if t == "" {
|
|
return tagging, nil
|
|
}
|
|
|
|
tagParts := strings.Split(t, "&")
|
|
for _, prt := range tagParts {
|
|
p := strings.Split(prt, "=")
|
|
if len(p) != 2 {
|
|
return nil, s3err.GetAPIError(s3err.ErrInvalidTag)
|
|
}
|
|
if len(p[0]) > 128 || len(p[1]) > 256 {
|
|
return nil, s3err.GetAPIError(s3err.ErrInvalidTag)
|
|
}
|
|
tagging[p[0]] = p[1]
|
|
}
|
|
|
|
return tagging, nil
|
|
}
|
|
|
|
func GetMultipartMD5(parts []types.CompletedPart) string {
|
|
var partsEtagBytes []byte
|
|
for _, part := range parts {
|
|
partsEtagBytes = append(partsEtagBytes, getEtagBytes(*part.ETag)...)
|
|
}
|
|
|
|
return fmt.Sprintf("\"%s-%d\"", md5String(partsEtagBytes), len(parts))
|
|
}
|
|
|
|
func getEtagBytes(etag string) []byte {
|
|
decode, err := hex.DecodeString(strings.ReplaceAll(etag, string('"'), ""))
|
|
if err != nil {
|
|
return []byte(etag)
|
|
}
|
|
return decode
|
|
}
|
|
|
|
func md5String(data []byte) string {
|
|
sum := md5.Sum(data)
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
type FileSectionReadCloser struct {
|
|
R io.Reader
|
|
F *os.File
|
|
}
|
|
|
|
func (f *FileSectionReadCloser) Read(p []byte) (int, error) {
|
|
return f.R.Read(p)
|
|
}
|
|
|
|
func (f *FileSectionReadCloser) Close() error {
|
|
return f.F.Close()
|
|
}
|