mirror of
https://github.com/versity/versitygw.git
synced 2026-09-25 09:24:22 +00:00
`CopyObject` treated a destination path equal to the source path as an in-place metadata rewrite, regardless of bucket versioning. On a versioned bucket a self copy therefore edited the current version and returned its existing version id instead of creating a new one. This also defeated object lock: `CheckObjectAccess` skips the retention and legal hold checks for overwrites on version-enabled buckets because an overwrite is expected to create a new version, so a `COMPLIANCE` retained or legal held version had its metadata replaced underneath it. A self copy is now rewritten in place only when the bucket is unversioned, and otherwise goes through the regular copy path, which creates a new version and leaves the one it replaces untouched. The rejection of a self copy that replaces nothing moved out of the in-place branch and is now applied only when the copy source carries no version id. Naming a version explicitly makes the request a regular copy, which AWS accepts even with the `COPY` metadata directive, while versitygw answered `InvalidRequest`. Source tagging for a `COPY` tagging directive is now read before `PutObject` writes the destination rather than after. A self copy replaces the source object's attributes, so the later read returned nothing and the tags were dropped from the new version.
7755 lines
237 KiB
Go
7755 lines
237 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 posix
|
|
|
|
import (
|
|
"context"
|
|
"crypto/md5"
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"hash"
|
|
"io"
|
|
"io/fs"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
|
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
|
"github.com/google/uuid"
|
|
"github.com/oklog/ulid/v2"
|
|
"github.com/versity/versitygw/auth"
|
|
"github.com/versity/versitygw/backend"
|
|
"github.com/versity/versitygw/backend/meta"
|
|
"github.com/versity/versitygw/debuglogger"
|
|
"github.com/versity/versitygw/s3api/middlewares"
|
|
"github.com/versity/versitygw/s3api/utils"
|
|
"github.com/versity/versitygw/s3err"
|
|
"github.com/versity/versitygw/s3response"
|
|
"golang.org/x/sync/semaphore"
|
|
)
|
|
|
|
type Posix struct {
|
|
backend.BackendUnsupported
|
|
|
|
// bucket/object metadata storage facility
|
|
meta meta.MetadataStorer
|
|
|
|
rootfd *os.File
|
|
rootdir string
|
|
// pathRoot is the directory bucket paths are built from: rootdir when
|
|
// PosixOpts.AbsolutePaths is set, otherwise "" because the process
|
|
// working directory is the root directory. See BucketPath.
|
|
pathRoot string
|
|
|
|
// chownuid/gid enable chowning of files to the account uid/gid
|
|
// when objects are uploaded
|
|
chownuid bool
|
|
chowngid bool
|
|
|
|
// euid/egid are the effective uid/gid of the running versitygw process
|
|
// used to determine if chowning is needed
|
|
euid int
|
|
egid int
|
|
|
|
// bucketlinks is a flag to enable symlinks to directories at the top
|
|
// level gateway directory to be treated as buckets the same as directories
|
|
bucketlinks bool
|
|
|
|
// bucket versioning directory path
|
|
versioningDir string
|
|
|
|
// newDirPerm is the permission to set on newly created directories
|
|
newDirPerm fs.FileMode
|
|
|
|
// newFilePerm is the permission to set on newly created object files
|
|
newFilePerm fs.FileMode
|
|
|
|
// forceNoTmpFile is a flag to disable the use of O_TMPFILE even
|
|
// if the filesystem supports it. This is needed for cases where
|
|
// there are different filesystems mounted below the bucket level.
|
|
forceNoTmpFile bool
|
|
|
|
// forceNoCopyFileRange is a flag to disable the use of io.Copy to
|
|
// reassemble multipart upload parts, which uses copy_file_range on
|
|
// linux. This is needed for cases where a filesystem that does not
|
|
// support copy_file_range is mounted over NFSv4.2.
|
|
forceNoCopyFileRange bool
|
|
|
|
// objectLockMode selects the advisory lock used for conditional object
|
|
// publishes. The local mode retains process-local serialization only, and
|
|
// the none mode rejects conditional writes.
|
|
objectLockMode ObjectLockMode
|
|
|
|
// enableODirect is a flag to open object data files with O_DIRECT.
|
|
// This is best-effort and falls back to buffered I/O when unsupported.
|
|
enableODirect bool
|
|
|
|
// enable posix level bucket name validations, not needed if the
|
|
// frontend handlers are already validating bucket names
|
|
validateBucketName bool
|
|
|
|
// defaultEtag is returned for objects that do not have an etag attribute
|
|
// stored (e.g. files placed on the filesystem outside of versitygw).
|
|
defaultEtag string
|
|
|
|
// actionLimiter limits the number of concurrently running POSIX actions.
|
|
// The primary goal is to bound OS thread growth: when goroutines block in
|
|
// filesystem syscalls, the Go scheduler may spawn additional OS threads to
|
|
// keep other goroutines runnable. Since nearly all POSIX actions eventually
|
|
// execute blocking syscalls (stat, readdir, xattr, open, etc.), this limiter
|
|
// constrains parallelism to prevent excessive thread creation under load.
|
|
actionLimiter *semaphore.Weighted
|
|
|
|
// copyObjectThreshold is the maximum allowed size (in bytes) for a copy
|
|
// source object. Requests to copy objects larger than this value are
|
|
// rejected with an 'InvalidRequest' to comply with the S3 limit
|
|
// of 5 GiB.
|
|
copyObjectThreshold int64
|
|
|
|
// ioBufferSize is the buffer size used by buffered copy/read paths.
|
|
ioBufferSize int
|
|
ioBufferPool sync.Pool
|
|
|
|
// dataIntegrityEtag, when true, replaces the standard MD5-based ETag with
|
|
// a checksum-derived ETag that embeds the algorithm name and value.
|
|
// For multipart uploads this applies to both part ETags and the final
|
|
// completed object ETag:
|
|
// single PUT: "ALGO-<checksum>"
|
|
// multipart part: "CRC64NVME-<part-checksum>"
|
|
// multipart CRC64NVME: "CRC64NVME-<whole-file-checksum>"
|
|
// multipart composite: "ALGO-<composite-checksum>-<part-count>"
|
|
dataIntegrityEtag bool
|
|
|
|
// objLockSlots are process-local striped slots taken alongside the shared
|
|
// per-bucket lock files by lockObjectPublish. Channels make local lock
|
|
// acquisition cancelable while avoiding filesystem lock thrash.
|
|
objLockSlots [objLockShards]chan struct{}
|
|
}
|
|
|
|
func (o *PosixOpts) SetNewDirPerm(perm fs.FileMode) {
|
|
o.NewDirPerm = perm
|
|
o.newDirPermSet = true
|
|
}
|
|
|
|
func (o *PosixOpts) SetNewFilePerm(perm fs.FileMode) {
|
|
o.NewFilePerm = perm
|
|
o.newFilePermSet = true
|
|
}
|
|
|
|
var _ backend.Backend = &Posix{}
|
|
|
|
const (
|
|
defaultNewDirPerm fs.FileMode = 0755
|
|
defaultNewFilePerm fs.FileMode = 0644
|
|
|
|
MetaTmpDir = ".sgwtmp"
|
|
MetaTmpMultipartDir = MetaTmpDir + "/multipart"
|
|
onameAttr = "objname"
|
|
inProgressSuffix = ".inprogress"
|
|
tagHdr = "X-Amz-Tagging"
|
|
oldMetaHdr = "X-Amz-Meta"
|
|
metadataHdr = "metadata"
|
|
contentTypeHdr = "content-type"
|
|
contentEncHdr = "content-encoding"
|
|
contentLangHdr = "content-language"
|
|
contentDispHdr = "content-disposition"
|
|
cacheCtrlHdr = "cache-control"
|
|
expiresHdr = "expires"
|
|
websiteRedirectHdr = "website-redirect-location"
|
|
emptyMD5 = "\"d41d8cd98f00b204e9800998ecf8427e\""
|
|
aclkey = "acl"
|
|
ownershipkey = "ownership"
|
|
etagkey = "etag"
|
|
checksumsKey = "checksums"
|
|
policykey = "policy"
|
|
bucketLockKey = "bucket-lock"
|
|
objectRetentionKey = "object-retention"
|
|
objectLegalHoldKey = "object-legal-hold"
|
|
corskey = "cors"
|
|
websitekey = "website"
|
|
versioningKey = "versioning"
|
|
deleteMarkerKey = "delete-marker"
|
|
versionIdKey = "version-id"
|
|
partCrc64nvme = "part-crc64nvme"
|
|
mpMetaKey = "mp-metadata"
|
|
|
|
nullVersionId = "null"
|
|
|
|
doFalloc = true
|
|
skipFalloc = false
|
|
|
|
odirectAllowed odirectPolicy = true
|
|
odirectNotAllowed odirectPolicy = false
|
|
|
|
// defaultConcurrency is the default limit for concurrent POSIX actions.
|
|
defaultConcurrency = 5000
|
|
// defaultIOBufferSize is the default buffer size used by io.CopyBuffer paths.
|
|
defaultIOBufferSize = 1024 * 1024
|
|
)
|
|
|
|
type odirectPolicy bool
|
|
|
|
// PosixOpts are the options for the Posix backend
|
|
type PosixOpts struct {
|
|
// ChownUID sets the UID of the object to the UID of the user on PUT
|
|
ChownUID bool
|
|
// ChownGID sets the GID of the object to the GID of the user on PUT
|
|
ChownGID bool
|
|
// BucketLinks enables symlinks to directories to be treated as buckets
|
|
BucketLinks bool
|
|
// AbsolutePaths makes the backend address every bucket and object by an
|
|
// absolute path under the root directory instead of changing the
|
|
// process working directory to the root and using relative paths. Use it
|
|
// when embedding the gateway in a process that must keep its working
|
|
// directory, such as a Go test that reads test data by relative path.
|
|
// Absolute paths cost slightly more per filesystem operation. A relative
|
|
// VersioningDir or SideCarDir is then resolved against the working
|
|
// directory rather than the root directory.
|
|
AbsolutePaths bool
|
|
//VersioningDir sets the version directory to enable object versioning
|
|
VersioningDir string
|
|
// NewDirPerm specifies the permission to set on newly created directories.
|
|
// When unset, the default is 0755. An explicit zero value (0000) is valid
|
|
// and is preserved when SetNewDirPerm(0000) is used.
|
|
NewDirPerm fs.FileMode
|
|
newDirPermSet bool
|
|
// NewFilePerm specifies the permission to set on newly created object
|
|
// files. Defaults to 0644 when unset. Unlike NewDirPerm, this mode is
|
|
// applied explicitly and is therefore not reduced by the process umask.
|
|
// An explicit zero value (0000) is valid and is preserved when
|
|
// SetNewFilePerm(0000) is used.
|
|
NewFilePerm fs.FileMode
|
|
newFilePermSet bool
|
|
// SideCarDir sets the directory to store sidecar metadata
|
|
SideCarDir string
|
|
// ForceNoTmpFile disables the use of O_TMPFILE even if the filesystem
|
|
// supports it
|
|
ForceNoTmpFile bool
|
|
// ForceNoCopyFileRange disables the use of io.Copy for multipart uploads parts
|
|
ForceNoCopyFileRange bool
|
|
// ForceNoObjLockFile is a deprecated compatibility alias for
|
|
// ObjectLockModeLocal.
|
|
ForceNoObjLockFile bool
|
|
// ObjectLockMode selects flock, fcntl, local, or none for conditional object
|
|
// publishes. An empty value defaults to flock. Local locking is only atomic
|
|
// within a single gateway process; none rejects conditional writes.
|
|
ObjectLockMode ObjectLockMode
|
|
// EnableODirect enables best-effort O_DIRECT for object data reads/writes.
|
|
// Disabled by default.
|
|
EnableODirect bool
|
|
// ValidateBucketNames enables minimal bucket name validation to prevent
|
|
// incorrect access to the filesystem. This is only needed if the
|
|
// frontend is not already validating bucket names.
|
|
ValidateBucketNames bool
|
|
// Concurrency sets the maximum number of concurrently running POSIX actions.
|
|
// It is intended to bound OS thread growth caused by goroutines blocking in
|
|
// filesystem-heavy syscalls (e.g., stat, readdir, xattr), which can otherwise
|
|
// trigger additional thread creation under load.
|
|
// This acts as a backpressure mechanism: once the limit is reached, new
|
|
// actions are queued until capacity becomes available. As a result, if the
|
|
// queue depth grows under sustained load, request latency increases and
|
|
// upstream timeouts may occur.
|
|
Concurrency int
|
|
// CopyObjectThreshold sets the maximum allowed source object size (in bytes)
|
|
// for CopyObject and UploadPartCopy operations. Requests exceeding this
|
|
// threshold are rejected with an 'InvalidRequest' error. Defaults to the
|
|
// S3 specification limit of 5 GiB.
|
|
CopyObjectThreshold int64
|
|
// DefaultEtag is returned for objects that do not have a stored etag
|
|
// attribute (e.g. files placed on the filesystem outside of versitygw).
|
|
// When empty, such objects are served with an empty ETag.
|
|
DefaultEtag string
|
|
// IOBufferSize sets the buffer size (in bytes) for copy/read paths that use
|
|
// io.CopyBuffer or buffered readers. Defaults to 1MiB when unset or invalid.
|
|
IOBufferSize int
|
|
// DataIntegrityEtag, when enabled, replaces the standard MD5-based ETag
|
|
// with a checksum-derived value that embeds the algorithm name and checksum
|
|
// (e.g. "CRC64NVME-<base64>"). For multipart uploads, part ETags become
|
|
// CRC64NVME-based values and the completed object ETag is checksum-derived.
|
|
// For CRC64NVME full-object checksums, the composable whole-file checksum is
|
|
// used; other algorithms embed the part count (e.g. "SHA256-<composite>-<N>").
|
|
DataIntegrityEtag bool
|
|
}
|
|
|
|
// New returns a Posix backend serving buckets from the directories under rootdir.
|
|
func New(rootdir string, ms meta.MetadataStorer, opts PosixOpts) (*Posix, error) {
|
|
objectLockMode, err := resolveObjectLockMode(opts.ObjectLockMode, opts.ForceNoObjLockFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
ioBufferSize := ioBufferSizeOrDefault(opts.IOBufferSize)
|
|
rootdirAbs, err := filepath.Abs(rootdir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get absolute path of %v: %w", rootdir, err)
|
|
}
|
|
|
|
if opts.SideCarDir != "" && strings.HasPrefix(opts.SideCarDir, rootdir) {
|
|
return nil, fmt.Errorf("sidecar directory cannot be inside the gateway root directory")
|
|
}
|
|
|
|
// A storer that keeps metadata on the object files (xattr) resolves
|
|
// bucket names the same way this backend does: relative to the working
|
|
// directory by default, under the absolute root with AbsolutePaths.
|
|
var pathRoot string
|
|
if opts.AbsolutePaths {
|
|
pathRoot = rootdirAbs
|
|
if rs, ok := ms.(meta.RootDirSetter); ok {
|
|
ms = rs.WithRootDir(rootdirAbs)
|
|
}
|
|
}
|
|
|
|
if !opts.AbsolutePaths {
|
|
// Change directory before resolving the versioning and sidecar
|
|
// directories: a relative one is then relative to the root, which is
|
|
// also how a SideCar storer constructed with the same relative path
|
|
// will resolve it once the process runs from the root.
|
|
err = os.Chdir(rootdirAbs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("chdir %v: %w", rootdir, err)
|
|
}
|
|
}
|
|
|
|
var versioningdirAbs string
|
|
// Ensure the versioning directory isn't within the root directory
|
|
if opts.VersioningDir != "" {
|
|
versioningdirAbs, err = validateSubDir(rootdirAbs, opts.VersioningDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
var sidecardirAbs string
|
|
// Ensure the sidecar directory isn't within the root directory
|
|
if opts.SideCarDir != "" {
|
|
sidecardirAbs, err = validateSubDir(rootdirAbs, opts.SideCarDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Opened last so that no error path below has to close it.
|
|
f, err := os.Open(rootdirAbs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open %v: %w", rootdir, err)
|
|
}
|
|
if opts.AbsolutePaths {
|
|
// Without the chdir above nothing else rejects a root that is not
|
|
// a directory.
|
|
if fi, err := f.Stat(); err != nil || !fi.IsDir() {
|
|
f.Close()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("stat %v: %w", rootdir, err)
|
|
}
|
|
return nil, fmt.Errorf("%v is not a directory", rootdir)
|
|
}
|
|
}
|
|
|
|
if versioningdirAbs != "" {
|
|
fmt.Println("Bucket versioning enabled with directory:", versioningdirAbs)
|
|
}
|
|
|
|
if sidecardirAbs != "" {
|
|
fmt.Println("Using sidecar directory for metadata:", sidecardirAbs)
|
|
}
|
|
|
|
// A gateway that is not root can only chown a file to an account whose
|
|
// uid/gid it already has, so --chuid/--chgid either do nothing or fail
|
|
// every write for every other account. That is a configuration mistake
|
|
// worth naming at startup rather than one InternalError per request, but
|
|
// it is not fatal: the process may hold CAP_CHOWN without being root, and
|
|
// a setup where every account shares the gateway's own uid/gid is a
|
|
// legitimate no-op.
|
|
euid, egid := os.Geteuid(), os.Getegid()
|
|
if (opts.ChownUID || opts.ChownGID) && euid != 0 {
|
|
fmt.Printf("Warning: --chuid/--chgid requested, but the gateway runs as euid %v/egid %v: "+
|
|
"writes for any account with a different uid/gid will fail unless this process can chown\n",
|
|
euid, egid)
|
|
}
|
|
|
|
newDirPerm := defaultNewDirPerm
|
|
if opts.newDirPermSet {
|
|
newDirPerm = opts.NewDirPerm.Perm()
|
|
}
|
|
newFilePerm := defaultNewFilePerm
|
|
if opts.newFilePermSet {
|
|
newFilePerm = opts.NewFilePerm.Perm()
|
|
}
|
|
|
|
p := &Posix{
|
|
meta: ms,
|
|
rootfd: f,
|
|
rootdir: rootdirAbs,
|
|
pathRoot: pathRoot,
|
|
euid: euid,
|
|
egid: egid,
|
|
chownuid: opts.ChownUID,
|
|
chowngid: opts.ChownGID,
|
|
bucketlinks: opts.BucketLinks,
|
|
versioningDir: versioningdirAbs,
|
|
newDirPerm: newDirPerm,
|
|
newFilePerm: newFilePerm,
|
|
forceNoTmpFile: opts.ForceNoTmpFile,
|
|
forceNoCopyFileRange: opts.ForceNoCopyFileRange,
|
|
objectLockMode: objectLockMode,
|
|
enableODirect: opts.EnableODirect,
|
|
validateBucketName: opts.ValidateBucketNames,
|
|
actionLimiter: semaphore.NewWeighted(int64(concurrencyOrDefault(opts.Concurrency))),
|
|
copyObjectThreshold: opts.CopyObjectThreshold,
|
|
defaultEtag: opts.DefaultEtag,
|
|
ioBufferSize: ioBufferSize,
|
|
ioBufferPool: sync.Pool{New: func() any {
|
|
b := make([]byte, ioBufferSize)
|
|
return &b
|
|
}},
|
|
dataIntegrityEtag: opts.DataIntegrityEtag,
|
|
objLockSlots: newObjLockSlots(),
|
|
}
|
|
if err := p.verifyObjectLockMode(); err != nil {
|
|
p.Shutdown()
|
|
return nil, err
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
// BucketPath returns the filesystem path of the bucket directory: the bucket
|
|
// name itself by default (the process working directory is the root
|
|
// directory), or the absolute path under the root with
|
|
// PosixOpts.AbsolutePaths.
|
|
//
|
|
// The versioning code passes the absolute path of a bucket's versioning
|
|
// directory where a bucket name is expected, and the metadata storer accepts
|
|
// the same substitution; an absolute bucket is therefore returned as given.
|
|
func (p *Posix) BucketPath(bucket string) string {
|
|
if p.pathRoot == "" || filepath.IsAbs(bucket) {
|
|
return bucket
|
|
}
|
|
return filepath.Join(p.pathRoot, bucket)
|
|
}
|
|
|
|
// rootPath returns the filesystem path of the root directory in the form
|
|
// BucketPath builds bucket paths from.
|
|
func (p *Posix) rootPath() string {
|
|
if p.pathRoot == "" {
|
|
return "."
|
|
}
|
|
return p.pathRoot
|
|
}
|
|
|
|
// ObjectPath returns the filesystem path of object within bucket. object is
|
|
// any path relative to the bucket directory: an object key, a multipart
|
|
// upload's temporary directory, or a part file within one.
|
|
func (p *Posix) ObjectPath(bucket, object string) string {
|
|
return filepath.Join(p.BucketPath(bucket), object)
|
|
}
|
|
|
|
// concurrencyOrDefault returns n if it is positive, otherwise defaultConcurrency.
|
|
func concurrencyOrDefault(n int) int {
|
|
if n > 0 {
|
|
return n
|
|
}
|
|
return defaultConcurrency
|
|
}
|
|
|
|
func ioBufferSizeOrDefault(n int) int {
|
|
if n > 0 {
|
|
return n
|
|
}
|
|
return defaultIOBufferSize
|
|
}
|
|
|
|
func (p *Posix) getIOBuffer() []byte {
|
|
bp, ok := p.ioBufferPool.Get().(*[]byte)
|
|
if !ok || bp == nil || cap(*bp) < p.ioBufferSize {
|
|
return make([]byte, p.ioBufferSize)
|
|
}
|
|
return (*bp)[:p.ioBufferSize]
|
|
}
|
|
|
|
func (p *Posix) putIOBuffer(b []byte) {
|
|
if b == nil {
|
|
return
|
|
}
|
|
if cap(b) < p.ioBufferSize {
|
|
return
|
|
}
|
|
b = b[:p.ioBufferSize]
|
|
p.ioBufferPool.Put(&b)
|
|
}
|
|
|
|
func validateSubDir(root, dir string) (string, error) {
|
|
absDir, err := filepath.Abs(dir)
|
|
if err != nil {
|
|
return "", fmt.Errorf("get absolute path of %v: %w",
|
|
dir, err)
|
|
}
|
|
|
|
if isDirBelowRoot(root, absDir) {
|
|
return "", fmt.Errorf("the root directory %v contains the directory %v",
|
|
root, dir)
|
|
}
|
|
|
|
vDir, err := os.Stat(absDir)
|
|
if err != nil {
|
|
return "", fmt.Errorf("stat %q: %w", absDir, err)
|
|
}
|
|
|
|
if !vDir.IsDir() {
|
|
return "", fmt.Errorf("path %q is not a directory", absDir)
|
|
}
|
|
|
|
return absDir, nil
|
|
}
|
|
|
|
func isDirBelowRoot(root, dir string) bool {
|
|
// Ensure the paths ends with a separator
|
|
if !strings.HasSuffix(root, string(filepath.Separator)) {
|
|
root += string(filepath.Separator)
|
|
}
|
|
|
|
if !strings.HasSuffix(dir, string(filepath.Separator)) {
|
|
dir += string(filepath.Separator)
|
|
}
|
|
|
|
// Ensure the root directory doesn't contain the directory
|
|
if strings.HasPrefix(dir, root) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (p *Posix) Shutdown() {
|
|
p.rootfd.Close()
|
|
}
|
|
|
|
func (p *Posix) String() string {
|
|
return "Posix Gateway"
|
|
}
|
|
|
|
// define a private key type
|
|
type ctxKey int
|
|
|
|
const (
|
|
ctxKeyNoAcquireSlot ctxKey = iota
|
|
)
|
|
|
|
// withCtxNoSlot is a context wrapper with ctxKeyNoAcquireSlot key
|
|
func withCtxNoSlot(ctx context.Context) context.Context {
|
|
return context.WithValue(ctx, ctxKeyNoAcquireSlot, struct{}{})
|
|
}
|
|
|
|
// acquireActionSlot reserves a concurrency slot for an action
|
|
// it blocks until a slot is available or the context is canceled.
|
|
// On success, it returns a releaser that must be called to free the slot.
|
|
// If ctxKeyNoAcquireSlot is set in the context, the limiter is bypassed
|
|
// and a no-op releaser is returned.
|
|
func (p *Posix) acquireActionSlot(ctx context.Context) (func(), error) {
|
|
// if no acquire lock is set, do not reserve a slot
|
|
// and return a blank releaser
|
|
val := ctx.Value(ctxKeyNoAcquireSlot)
|
|
if val != nil {
|
|
return func() {}, nil
|
|
}
|
|
|
|
if err := p.actionLimiter.Acquire(ctx, 1); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return func() {
|
|
p.actionLimiter.Release(1)
|
|
}, nil
|
|
}
|
|
|
|
// returns the versioning state
|
|
func (p *Posix) versioningEnabled() bool {
|
|
return p.versioningDir != ""
|
|
}
|
|
|
|
// validateVersionId checks if the input versionId is 'ulid' compatible
|
|
func (p *Posix) validateVersionId(versionId string) error {
|
|
if versionId == "" || versionId == "null" {
|
|
return nil
|
|
}
|
|
_, err := ulid.Parse(versionId)
|
|
if err != nil {
|
|
return s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, versionId)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DoesBucketExist returns NoSuchBucket unless bucket names a bucket. Backends
|
|
// built on Posix that add their own entry points must check buckets with it
|
|
// rather than stat the bucket path themselves.
|
|
func (p *Posix) DoesBucketExist(bucket string) error {
|
|
return p.doesBucketExist(bucket)
|
|
}
|
|
|
|
// doesBucketExist returns NoSuchBucket unless bucket names a directory under
|
|
// the root, or with bucketlinks a symlink to one: the entries
|
|
// listBucketFileInfos returns. The root of a dataset the gateway did not
|
|
// create may also hold regular files, symlinks and other entries; a request
|
|
// naming one of them fails like one naming a missing bucket instead of
|
|
// storing bucket metadata on it or building object paths through it.
|
|
func (p *Posix) doesBucketExist(bucket string) error {
|
|
path := p.BucketPath(bucket)
|
|
fi, err := os.Lstat(path)
|
|
if err == nil && p.bucketlinks && fi.Mode()&fs.ModeSymlink != 0 {
|
|
fi, err = os.Stat(path)
|
|
}
|
|
// a symlink loop never resolves to a directory
|
|
if errors.Is(err, fs.ErrNotExist) || errors.Is(err, syscall.ELOOP) {
|
|
return s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("stat bucket: %w", err)
|
|
}
|
|
if !fi.IsDir() {
|
|
return s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// doesBucketAndObjectExist checks that bucket exists and, when versionId is
|
|
// empty, that object has a current version. A specific version is looked up
|
|
// once versionId is validated.
|
|
func (p *Posix) doesBucketAndObjectExist(bucket, object, versionId string) error {
|
|
err := p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if versionId != "" {
|
|
return nil
|
|
}
|
|
|
|
_, _, err = p.objVersionAttrPath(bucket, object, "")
|
|
return err
|
|
}
|
|
|
|
func (p *Posix) ListBuckets(ctx context.Context, input s3response.ListBucketsInput) (s3response.ListAllMyBucketsResult, error) {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return s3response.ListAllMyBucketsResult{}, err
|
|
}
|
|
defer release()
|
|
|
|
fis, err := listBucketFileInfos(p.rootPath(), p.bucketlinks)
|
|
if err != nil {
|
|
return s3response.ListAllMyBucketsResult{}, fmt.Errorf("listBucketFileInfos : %w", err)
|
|
}
|
|
|
|
var cToken string
|
|
|
|
var buckets []s3response.ListAllMyBucketsEntry
|
|
for _, fi := range fis {
|
|
if !strings.HasPrefix(fi.Name(), input.Prefix) {
|
|
continue
|
|
}
|
|
|
|
if !utils.IsValidBucketName(fi.Name()) {
|
|
continue
|
|
}
|
|
|
|
if len(buckets) == int(input.MaxBuckets) {
|
|
cToken = buckets[len(buckets)-1].Name
|
|
break
|
|
}
|
|
|
|
if fi.Name() <= input.ContinuationToken {
|
|
continue
|
|
}
|
|
|
|
// return all the buckets for admin users
|
|
if input.IsAdmin {
|
|
buckets = append(buckets, s3response.ListAllMyBucketsEntry{
|
|
Name: fi.Name(),
|
|
CreationDate: fi.ModTime(),
|
|
})
|
|
continue
|
|
}
|
|
|
|
aclJSON, err := p.meta.RetrieveAttribute(nil, fi.Name(), "", aclkey)
|
|
if errors.Is(err, meta.ErrNoSuchKey) {
|
|
// skip buckets without acl tag
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return s3response.ListAllMyBucketsResult{}, fmt.Errorf("get acl tag: %w", err)
|
|
}
|
|
|
|
acl, err := auth.ParseACL(aclJSON)
|
|
if err != nil {
|
|
return s3response.ListAllMyBucketsResult{}, err
|
|
}
|
|
|
|
if acl.Owner == input.Owner {
|
|
buckets = append(buckets, s3response.ListAllMyBucketsEntry{
|
|
Name: fi.Name(),
|
|
CreationDate: fi.ModTime(),
|
|
})
|
|
}
|
|
}
|
|
|
|
return s3response.ListAllMyBucketsResult{
|
|
Buckets: s3response.ListAllMyBucketsList{
|
|
Bucket: buckets,
|
|
},
|
|
Owner: s3response.CanonicalUser{
|
|
ID: input.Owner,
|
|
},
|
|
Prefix: input.Prefix,
|
|
ContinuationToken: cToken,
|
|
}, nil
|
|
}
|
|
|
|
// IsBucketValid reports whether bucket may be used as a bucket name by this
|
|
// backend. Backends built on Posix that add their own entry points must check
|
|
// bucket names with it before building paths from them.
|
|
func (p *Posix) IsBucketValid(bucket string) bool {
|
|
return p.isBucketValid(bucket)
|
|
}
|
|
|
|
func (p *Posix) isBucketValid(bucket string) bool {
|
|
// Regardless of validation mode, a bucket name must name a single
|
|
// directory entry under the root: "", "." and ".." would resolve to the
|
|
// root or its parent (see BucketPath), a separator or an absolute path
|
|
// would escape it. Callers that need the versioning directory pass it in
|
|
// place of the bucket only after this check.
|
|
if bucket == "" || bucket == "." || bucket == ".." || bucket == objLockDir ||
|
|
filepath.IsAbs(bucket) || strings.ContainsRune(bucket, '/') ||
|
|
strings.ContainsRune(bucket, os.PathSeparator) {
|
|
return false
|
|
}
|
|
|
|
if !p.validateBucketName {
|
|
return true
|
|
}
|
|
|
|
return backend.IsValidDirectoryName(bucket)
|
|
}
|
|
|
|
func (p *Posix) HeadBucket(ctx context.Context, input *s3.HeadBucketInput) (*s3.HeadBucketOutput, error) {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer release()
|
|
if !p.isBucketValid(*input.Bucket) {
|
|
return nil, s3err.GetBucketErr(s3err.ErrInvalidBucketName, *input.Bucket)
|
|
}
|
|
err = p.doesBucketExist(*input.Bucket)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &s3.HeadBucketOutput{}, nil
|
|
}
|
|
|
|
func (p *Posix) CreateBucket(ctx context.Context, input *s3.CreateBucketInput, acl []byte) (err error) {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer release()
|
|
|
|
acct, ok := ctx.Value("bucket-owner").(auth.Account)
|
|
if !ok {
|
|
acct = auth.Account{}
|
|
}
|
|
|
|
uid, gid, doChown := p.getChownIDs(acct)
|
|
|
|
bucket := *input.Bucket
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
|
|
tagging, err := backend.ParseCreateBucketTags(input.CreateBucketConfiguration.Tags)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = os.Mkdir(p.BucketPath(bucket), p.newDirPerm)
|
|
if err != nil && os.IsExist(err) {
|
|
// An existing entry that is not a bucket, such as a regular file at
|
|
// the root of a preexisting dataset, still takes the name but has
|
|
// no bucket metadata to consult.
|
|
err := p.doesBucketExist(bucket)
|
|
if errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchBucket)) {
|
|
return s3err.GetBucketErr(s3err.ErrBucketAlreadyExists, bucket)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
aclJSON, err := p.meta.RetrieveAttribute(nil, bucket, "", aclkey)
|
|
if errors.Is(err, meta.ErrNoSuchKey) {
|
|
// The directory already exists but has no gateway-managed acl
|
|
// attribute, e.g. a preexisting directory on a dataset the
|
|
// gateway was pointed at rather than one created via CreateBucket
|
|
return s3err.GetBucketErr(s3err.ErrBucketAlreadyExists, bucket)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("get bucket acl: %w", err)
|
|
}
|
|
|
|
acl, err := auth.ParseACL(aclJSON)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if acl.Owner == acct.Access {
|
|
return s3err.GetBucketErr(s3err.ErrBucketAlreadyOwnedByYou, bucket)
|
|
}
|
|
return s3err.GetBucketErr(s3err.ErrBucketAlreadyExists, bucket)
|
|
}
|
|
if err != nil {
|
|
if errors.Is(err, syscall.EROFS) {
|
|
return s3err.GetAPIError(s3err.ErrMethodNotAllowed)
|
|
}
|
|
return fmt.Errorf("mkdir bucket: %w", err)
|
|
}
|
|
|
|
// The directory now exists but is not yet a usable bucket: until the acl
|
|
// xattr below is stored, every request for this name — a retry of this
|
|
// same CreateBucket included — fails with "get bucket acl: no such key",
|
|
// and the name stays poisoned until someone removes the directory by
|
|
// hand. Undo the mkdir on any failure from here on so the name stays
|
|
// retryable.
|
|
defer func() {
|
|
if err == nil {
|
|
return
|
|
}
|
|
p.removePartialBucket(bucket)
|
|
}()
|
|
|
|
if doChown {
|
|
err = os.Chown(p.BucketPath(bucket), uid, gid)
|
|
if err != nil {
|
|
return p.chownErr(bucket, uid, gid, err)
|
|
}
|
|
}
|
|
|
|
err = p.meta.StoreAttribute(nil, bucket, "", aclkey, acl)
|
|
if err != nil {
|
|
return fmt.Errorf("set acl: %w", err)
|
|
}
|
|
err = p.meta.StoreAttribute(nil, bucket, "", ownershipkey, []byte(input.ObjectOwnership))
|
|
if err != nil {
|
|
return fmt.Errorf("set ownership: %w", err)
|
|
}
|
|
if tagging != nil {
|
|
tags, err := json.Marshal(tagging)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal tags: %w", err)
|
|
}
|
|
err = p.meta.StoreAttribute(nil, bucket, "", tagHdr, tags)
|
|
if err != nil {
|
|
return fmt.Errorf("set tags: %w", err)
|
|
}
|
|
}
|
|
|
|
if input.ObjectLockEnabledForBucket != nil && *input.ObjectLockEnabledForBucket {
|
|
// First enable bucket versioning
|
|
// Bucket versioning is enabled automatically with object lock
|
|
if p.versioningEnabled() {
|
|
err = p.PutBucketVersioning(withCtxNoSlot(ctx), bucket, types.BucketVersioningStatusEnabled)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
now := time.Now()
|
|
defaultLock := auth.BucketLockConfig{
|
|
Enabled: true,
|
|
CreatedAt: &now,
|
|
}
|
|
|
|
defaultLockParsed, err := json.Marshal(defaultLock)
|
|
if err != nil {
|
|
return fmt.Errorf("parse default bucket lock state: %w", err)
|
|
}
|
|
|
|
err = p.meta.StoreAttribute(nil, bucket, "", bucketLockKey, defaultLockParsed)
|
|
if err != nil {
|
|
return fmt.Errorf("set default bucket lock: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// removePartialBucket undoes a partially completed CreateBucket: the bucket
|
|
// directory, any metadata stored for it (which may live in a sidecar
|
|
// directory outside the bucket) and its versioning directory. Failures here
|
|
// are logged rather than returned — the caller is already failing with the
|
|
// error that matters, and reporting a cleanup failure instead would hide it.
|
|
func (p *Posix) removePartialBucket(bucket string) {
|
|
if err := os.RemoveAll(p.BucketPath(bucket)); err != nil {
|
|
debuglogger.Logf("failed to remove partially created bucket (%q): %v", bucket, err)
|
|
}
|
|
if err := p.meta.DeleteAttributes(bucket, ""); err != nil {
|
|
debuglogger.Logf("failed to delete partially created bucket sidecar attributes (%q): %v", bucket, err)
|
|
}
|
|
if p.versioningEnabled() {
|
|
err := os.RemoveAll(filepath.Join(p.versioningDir, bucket))
|
|
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
debuglogger.Logf("failed to remove partially created bucket version directory (%q): %v", bucket, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// mkdirAll is backend.MkdirAll with this backend's directory permissions and
|
|
// the same chown annotation CreateBucket uses: an EPERM here is the
|
|
// --chuid/--chgid misconfiguration, not a filesystem problem. Only EPERM is
|
|
// rewritten, so callers matching on EROFS or ErrObjectParentIsFile are
|
|
// unaffected.
|
|
func (p *Posix) mkdirAll(path string, uid, gid int, doChown bool) error {
|
|
err := backend.MkdirAll(path, uid, gid, doChown, p.newDirPerm)
|
|
if doChown && errors.Is(err, syscall.EPERM) {
|
|
return p.chownErr(path, uid, gid, err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// chownErr annotates a chown failure with the configuration that asked for
|
|
// it. EPERM is not transient here: an unprivileged gateway can never give a
|
|
// file away to another uid or to a group it is not in, so every write for
|
|
// that account fails the same way until the configuration changes. Name the
|
|
// flags and the process identity that make the request impossible rather
|
|
// than leaving a bare "operation not permitted" in the log.
|
|
func (p *Posix) chownErr(name string, uid, gid int, err error) error {
|
|
// The errno is wrapped rather than the *fs.PathError it arrived in: the
|
|
// PathError repeats a path this message already names, and errors.Is
|
|
// still matches EPERM either way.
|
|
var errno syscall.Errno
|
|
if errors.As(err, &errno) && errno == syscall.EPERM {
|
|
return fmt.Errorf("chown %v to %v:%v: %w: --chuid/--chgid need a privileged gateway, but this one runs as euid %v/egid %v",
|
|
name, uid, gid, errno, p.euid, p.egid)
|
|
}
|
|
return fmt.Errorf("chown %v: %w", name, err)
|
|
}
|
|
|
|
func (p *Posix) isBucketEmpty(bucket string) error {
|
|
if p.versioningEnabled() {
|
|
ents, err := os.ReadDir(filepath.Join(p.versioningDir, bucket))
|
|
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
return fmt.Errorf("readdir bucket: %w", err)
|
|
}
|
|
if err == nil {
|
|
if len(ents) == 1 && ents[0].Name() != MetaTmpDir {
|
|
return s3err.GetBucketErr(s3err.ErrVersionedBucketNotEmpty, bucket)
|
|
} else if len(ents) > 1 {
|
|
return s3err.GetBucketErr(s3err.ErrVersionedBucketNotEmpty, bucket)
|
|
}
|
|
}
|
|
}
|
|
|
|
ents, err := os.ReadDir(p.BucketPath(bucket))
|
|
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
return fmt.Errorf("readdir bucket: %w", err)
|
|
}
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket)
|
|
}
|
|
if len(ents) == 1 && ents[0].Name() != MetaTmpDir {
|
|
return s3err.GetBucketErr(s3err.ErrBucketNotEmpty, bucket)
|
|
} else if len(ents) > 1 {
|
|
return s3err.GetBucketErr(s3err.ErrBucketNotEmpty, bucket)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Posix) DeleteBucket(ctx context.Context, bucket string) error {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer release()
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
err = p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Check if the bucket is empty
|
|
err = p.isBucketEmpty(bucket)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Remove the bucket
|
|
err = os.RemoveAll(p.BucketPath(bucket))
|
|
if err != nil {
|
|
return fmt.Errorf("remove bucket: %w", err)
|
|
}
|
|
// Bucket data is already removed; a metadata cleanup failure orphans only
|
|
// the sidecar directory, which is not user-visible. Log and continue rather
|
|
// than returning an error that would mislead callers into thinking the
|
|
// bucket still exists.
|
|
if err = p.meta.DeleteAttributes(bucket, ""); err != nil {
|
|
debuglogger.Logf("failed to delete bucket sidecar attributes (%q): %v", bucket, err)
|
|
}
|
|
// Remove the bucket from versioning directory
|
|
if p.versioningEnabled() {
|
|
err = os.RemoveAll(filepath.Join(p.versioningDir, bucket))
|
|
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
return fmt.Errorf("remove bucket version: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Posix) PutBucketOwnershipControls(ctx context.Context, bucket string, ownership types.ObjectOwnership) error {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer release()
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
err = p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = p.meta.StoreAttribute(nil, bucket, "", ownershipkey, []byte(ownership))
|
|
if err != nil {
|
|
return fmt.Errorf("set ownership: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
func (p *Posix) GetBucketOwnershipControls(ctx context.Context, bucket string) (types.ObjectOwnership, error) {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer release()
|
|
|
|
var ownship types.ObjectOwnership
|
|
if !p.isBucketValid(bucket) {
|
|
return ownship, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
err = p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return ownship, err
|
|
}
|
|
|
|
ownership, err := p.meta.RetrieveAttribute(nil, bucket, "", ownershipkey)
|
|
if errors.Is(err, meta.ErrNoSuchKey) {
|
|
return ownship, s3err.GetBucketErr(s3err.ErrOwnershipControlsNotFound, bucket)
|
|
}
|
|
if err != nil {
|
|
return ownship, fmt.Errorf("get bucket ownership status: %w", err)
|
|
}
|
|
|
|
return types.ObjectOwnership(ownership), nil
|
|
}
|
|
func (p *Posix) DeleteBucketOwnershipControls(ctx context.Context, bucket string) error {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer release()
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
err = p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = p.meta.DeleteAttribute(bucket, "", ownershipkey)
|
|
if err != nil {
|
|
if errors.Is(err, meta.ErrNoSuchKey) {
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("delete ownership: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Posix) PutBucketVersioning(ctx context.Context, bucket string, status types.BucketVersioningStatus) error {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer release()
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
if !p.versioningEnabled() {
|
|
return s3err.GetAPIError(s3err.ErrVersioningNotConfigured)
|
|
}
|
|
err = p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Store 1 bit for bucket versioning state
|
|
var versioning []byte
|
|
switch status {
|
|
case types.BucketVersioningStatusEnabled:
|
|
// '1' maps to 'Enabled'
|
|
versioning = []byte{1}
|
|
case types.BucketVersioningStatusSuspended:
|
|
lockRaw, err := p.GetObjectLockConfiguration(withCtxNoSlot(ctx), bucket)
|
|
if err != nil && !errors.Is(err, s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotFound)) {
|
|
return err
|
|
}
|
|
if err == nil {
|
|
lockStatus, err := auth.ParseBucketLockConfigurationOutput(lockRaw)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if lockStatus.ObjectLockEnabled == types.ObjectLockEnabledEnabled {
|
|
return s3err.GetAPIError(s3err.ErrSuspendedVersioningNotAllowed)
|
|
}
|
|
}
|
|
|
|
// '0' maps to 'Suspended'
|
|
versioning = []byte{0}
|
|
}
|
|
|
|
err = p.meta.StoreAttribute(nil, bucket, "", versioningKey, versioning)
|
|
if err != nil {
|
|
return fmt.Errorf("set versioning: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Posix) GetBucketVersioning(ctx context.Context, bucket string) (s3response.GetBucketVersioningOutput, error) {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return s3response.GetBucketVersioningOutput{}, err
|
|
}
|
|
defer release()
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return s3response.GetBucketVersioningOutput{}, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
|
|
err = p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return s3response.GetBucketVersioningOutput{}, err
|
|
}
|
|
|
|
if !p.versioningEnabled() {
|
|
// No versioning directory is configured: report the bucket as
|
|
// never-versioned (200 with an empty configuration) to match AWS,
|
|
// instead of failing with VersioningNotConfigured. GUI clients
|
|
// (S3 Browser, CloudBerry, ...) poll this API while browsing
|
|
// buckets. PutBucketVersioning still rejects the request.
|
|
return s3response.GetBucketVersioningOutput{}, nil
|
|
}
|
|
|
|
vData, err := p.meta.RetrieveAttribute(nil, bucket, "", versioningKey)
|
|
if errors.Is(err, meta.ErrNoSuchKey) {
|
|
return s3response.GetBucketVersioningOutput{}, nil
|
|
} else if err != nil {
|
|
return s3response.GetBucketVersioningOutput{}, fmt.Errorf("get bucket versioning config: %w", err)
|
|
}
|
|
|
|
enabled, suspended := types.BucketVersioningStatusEnabled, types.BucketVersioningStatusSuspended
|
|
switch vData[0] {
|
|
case 1:
|
|
return s3response.GetBucketVersioningOutput{
|
|
Status: &enabled,
|
|
}, nil
|
|
case 0:
|
|
return s3response.GetBucketVersioningOutput{
|
|
Status: &suspended,
|
|
}, nil
|
|
}
|
|
|
|
return s3response.GetBucketVersioningOutput{}, nil
|
|
}
|
|
|
|
// Returns the specified bucket versioning status
|
|
func (p *Posix) getBucketVersioningStatus(ctx context.Context, bucket string) (types.BucketVersioningStatus, error) {
|
|
res, err := p.GetBucketVersioning(withCtxNoSlot(ctx), bucket)
|
|
if errors.Is(err, s3err.GetAPIError(s3err.ErrVersioningNotConfigured)) {
|
|
return "", nil
|
|
}
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if res.Status == nil {
|
|
return "", nil
|
|
}
|
|
|
|
return *res.Status, nil
|
|
}
|
|
|
|
// Checks if the given bucket versioning status is 'Enabled'
|
|
func (p *Posix) isBucketVersioningEnabled(s types.BucketVersioningStatus) bool {
|
|
return s == types.BucketVersioningStatusEnabled
|
|
}
|
|
|
|
// Checks if the given bucket versioning status is 'Suspended'
|
|
func (p *Posix) isBucketVersioningSuspended(s types.BucketVersioningStatus) bool {
|
|
return s == types.BucketVersioningStatusSuspended
|
|
}
|
|
|
|
// Generates the object version path in the versioning directory
|
|
func (p *Posix) genObjVersionPath(bucket, key string) string {
|
|
return filepath.Join(p.versioningDir, bucket, genObjVersionKey(key))
|
|
}
|
|
|
|
// Generates the versioning path for the given object key
|
|
func genObjVersionKey(key string) string {
|
|
sum := fmt.Sprintf("%x", sha256.Sum256([]byte(key)))
|
|
|
|
return filepath.Join(sum[:2], sum[2:4], sum[4:6], sum)
|
|
}
|
|
|
|
// Removes the null versionId object from versioning directory
|
|
func (p *Posix) deleteNullVersionIdObject(bucket, key string) error {
|
|
versionPath := filepath.Join(p.genObjVersionPath(bucket, key), nullVersionId)
|
|
|
|
err := os.Remove(versionPath)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_ = p.meta.DeleteAttributes(versionPath, "")
|
|
return nil
|
|
}
|
|
|
|
func isRemovableAttr(attr string) bool {
|
|
switch attr {
|
|
case objectLegalHoldKey, objectRetentionKey:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// dirObjectAttrs are the attributes that make up the state of a directory
|
|
// object. The directory is kept across versions and is also the parent of
|
|
// other objects, so it may carry attributes that don't belong to the object:
|
|
// only these are copied into, restored from and cleared between versions.
|
|
// An attribute stored on objects must also be listed here.
|
|
var dirObjectAttrs = []string{
|
|
etagkey,
|
|
checksumsKey,
|
|
contentTypeHdr,
|
|
contentEncHdr,
|
|
contentLangHdr,
|
|
contentDispHdr,
|
|
cacheCtrlHdr,
|
|
expiresHdr,
|
|
websiteRedirectHdr,
|
|
metadataHdr,
|
|
tagHdr,
|
|
objectLegalHoldKey,
|
|
objectRetentionKey,
|
|
versionIdKey,
|
|
deleteMarkerKey,
|
|
}
|
|
|
|
// isDirObject reports whether the directory at bucket/key is a directory
|
|
// object: only directories created with a put carry an etag
|
|
func (p *Posix) isDirObject(bucket, key string) (bool, error) {
|
|
_, err := p.meta.RetrieveAttribute(nil, bucket, key, etagkey)
|
|
if errors.Is(err, meta.ErrNoSuchKey) || errors.Is(err, fs.ErrNotExist) {
|
|
return false, nil
|
|
}
|
|
if err != nil {
|
|
return false, fmt.Errorf("get dir etag: %w", err)
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// isLiveObject reports whether fi, the entry at the path of key, is the
|
|
// current version of key. A key and the same key with a trailing slash
|
|
// share one path and one set of attributes: a file there is the object of
|
|
// the key without the slash, a directory object the object of the key with
|
|
// it.
|
|
func (p *Posix) isLiveObject(fi os.FileInfo, bucket, key string) (bool, error) {
|
|
if fi.IsDir() != strings.HasSuffix(key, "/") {
|
|
return false, nil
|
|
}
|
|
if !fi.IsDir() {
|
|
return true, nil
|
|
}
|
|
return p.isDirObject(bucket, key)
|
|
}
|
|
|
|
// statLiveObject returns the file info of the current version of key. An
|
|
// error matching fs.ErrNotExist is returned when key has no current
|
|
// version, including when the entry at its path is another key's object.
|
|
func (p *Posix) statLiveObject(bucket, key string) (os.FileInfo, error) {
|
|
fi, err := os.Stat(p.ObjectPath(bucket, key))
|
|
if isErrNotDir(err) {
|
|
return nil, fs.ErrNotExist
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
isObj, err := p.isLiveObject(fi, bucket, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !isObj {
|
|
return nil, fs.ErrNotExist
|
|
}
|
|
return fi, nil
|
|
}
|
|
|
|
// objVersionAttrPath returns the bucket and object that the attributes of
|
|
// the version versionId of key are stored at: key itself for its current
|
|
// version, an entry of the versioning directory for other versions. An
|
|
// empty versionId selects the current version, and NoSuchKey is returned if
|
|
// key has none.
|
|
func (p *Posix) objVersionAttrPath(bucket, key, versionId string) (string, string, error) {
|
|
_, err := p.statLiveObject(bucket, key)
|
|
if isErrNameTooLong(err) {
|
|
return "", "", s3err.GetKeyTooLongErr(int64(len(key)), 1024)
|
|
}
|
|
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
return "", "", fmt.Errorf("stat object: %w", err)
|
|
}
|
|
isLive := err == nil
|
|
|
|
if versionId == "" {
|
|
if !isLive {
|
|
return "", "", s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
return bucket, key, nil
|
|
}
|
|
|
|
if isLive {
|
|
vId, err := p.meta.RetrieveAttribute(nil, bucket, key, versionIdKey)
|
|
if errors.Is(err, meta.ErrNoSuchKey) {
|
|
vId = []byte(nullVersionId)
|
|
} else if err != nil {
|
|
return "", "", fmt.Errorf("get obj versionId: %w", err)
|
|
}
|
|
if string(vId) == versionId {
|
|
return bucket, key, nil
|
|
}
|
|
}
|
|
|
|
return filepath.Join(p.versioningDir, bucket), filepath.Join(genObjVersionKey(key), versionId), nil
|
|
}
|
|
|
|
// clearDirObjectAttrs removes the directory object attributes, including
|
|
// legacy metadata attributes, from the directory at bucket/key. The etag is
|
|
// kept: it marks the directory as an object, so a failure before the new
|
|
// attributes are stored leaves the object and its versions visible.
|
|
func (p *Posix) clearDirObjectAttrs(bucket, key string) error {
|
|
attrs, err := p.meta.ListAttributes(bucket, key)
|
|
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
return fmt.Errorf("list object attributes: %w", err)
|
|
}
|
|
for _, attr := range attrs {
|
|
if isValidMeta(attr) {
|
|
err := p.meta.DeleteAttribute(bucket, key, attr)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return fmt.Errorf("remove %v attribute: %w", attr, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, attr := range dirObjectAttrs {
|
|
if attr == etagkey {
|
|
continue
|
|
}
|
|
err := p.meta.DeleteAttribute(bucket, key, attr)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) && !errors.Is(err, fs.ErrNotExist) {
|
|
return fmt.Errorf("remove %v attribute: %w", attr, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Creates a new copy(version) of an object in the versioning directory
|
|
func (p *Posix) createObjVersion(bucket, key string, size int64, acc auth.Account, removeAttributes bool) (versionPath string, err error) {
|
|
sf, err := os.Open(p.ObjectPath(bucket, key))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer sf.Close()
|
|
|
|
var versionId string
|
|
data, err := p.meta.RetrieveAttribute(sf, bucket, key, versionIdKey)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return versionPath, fmt.Errorf("get object versionId: %w", err)
|
|
}
|
|
if err == nil {
|
|
versionId = string(data)
|
|
} else {
|
|
versionId = nullVersionId
|
|
}
|
|
|
|
// a directory object version is an empty file carrying the
|
|
// directory object attributes
|
|
isDir := strings.HasSuffix(key, "/")
|
|
attrs := dirObjectAttrs
|
|
if isDir {
|
|
size = 0
|
|
// store legacy metadata attributes as the metadata attribute
|
|
p.loadObjectMetadata(sf, bucket, key)
|
|
} else {
|
|
attrs, err = p.meta.ListAttributes(bucket, key)
|
|
if err != nil {
|
|
return versionPath, fmt.Errorf("load object attributes: %w", err)
|
|
}
|
|
}
|
|
|
|
versionBucketPath := filepath.Join(p.versioningDir, bucket)
|
|
versioningKey := filepath.Join(genObjVersionKey(key), versionId)
|
|
versionTmpPath := filepath.Join(versionBucketPath, MetaTmpDir)
|
|
f, err := p.openTmpFile(versionTmpPath, versionBucketPath, versioningKey,
|
|
size, acc, doFalloc, p.forceNoTmpFile, odirectNotAllowed)
|
|
if err != nil {
|
|
return versionPath, err
|
|
}
|
|
defer f.cleanup()
|
|
|
|
originalMTime := time.Now()
|
|
if srcInfo, err := sf.Stat(); err == nil {
|
|
originalMTime = srcInfo.ModTime()
|
|
}
|
|
|
|
if !isDir {
|
|
// Prioritize copy_file_range for internal file-to-file version copies.
|
|
_, err = io.Copy(f.File(), sf)
|
|
if err != nil {
|
|
return versionPath, err
|
|
}
|
|
}
|
|
|
|
// Restore original mtime after copy
|
|
err = os.Chtimes(f.File().Name(), time.Now(), originalMTime)
|
|
if err != nil {
|
|
return versionPath, err
|
|
}
|
|
|
|
versionPath = filepath.Join(versionBucketPath, versioningKey)
|
|
|
|
err = os.MkdirAll(filepath.Join(versionBucketPath, genObjVersionKey(key)), p.newDirPerm)
|
|
if err != nil {
|
|
return versionPath, err
|
|
}
|
|
|
|
// Copy the object attributes(metadata)
|
|
for _, attr := range attrs {
|
|
data, err := p.meta.RetrieveAttribute(sf, bucket, key, attr)
|
|
if isDir && errors.Is(err, meta.ErrNoSuchKey) {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return versionPath, fmt.Errorf("list %v attribute: %w", attr, err)
|
|
}
|
|
|
|
err = p.meta.StoreAttribute(f.File(), versionPath, "", attr, data)
|
|
if err != nil {
|
|
return versionPath, fmt.Errorf("store %v attribute: %w", attr, err)
|
|
}
|
|
|
|
// remove object lock attributes in delete marker
|
|
if removeAttributes && isRemovableAttr(attr) {
|
|
err := p.meta.DeleteAttribute(bucket, key, attr)
|
|
if err != nil {
|
|
return versionPath, fmt.Errorf("remove %s attribute: %w", attr, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := f.link(); err != nil {
|
|
return versionPath, err
|
|
}
|
|
|
|
return versionPath, nil
|
|
}
|
|
|
|
func (p *Posix) ListObjectVersions(ctx context.Context, input *s3.ListObjectVersionsInput) (s3response.ListVersionsResult, error) {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return s3response.ListVersionsResult{}, err
|
|
}
|
|
defer release()
|
|
|
|
bucket := *input.Bucket
|
|
var prefix, delim, keyMarker, versionIdMarker string
|
|
var max int
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return s3response.ListVersionsResult{}, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
|
|
if input.Prefix != nil {
|
|
prefix = *input.Prefix
|
|
}
|
|
if input.Delimiter != nil {
|
|
delim = *input.Delimiter
|
|
}
|
|
if input.KeyMarker != nil {
|
|
keyMarker = *input.KeyMarker
|
|
}
|
|
if input.VersionIdMarker != nil {
|
|
versionIdMarker = *input.VersionIdMarker
|
|
}
|
|
if input.MaxKeys != nil {
|
|
max = int(*input.MaxKeys)
|
|
}
|
|
|
|
err = p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return s3response.ListVersionsResult{}, err
|
|
}
|
|
|
|
fileSystem := os.DirFS(p.BucketPath(bucket))
|
|
results, err := backend.WalkVersions(ctx, fileSystem, prefix, delim, keyMarker, versionIdMarker, max,
|
|
p.fileToObjVersions(bucket), []string{MetaTmpDir})
|
|
if err != nil {
|
|
return s3response.ListVersionsResult{}, fmt.Errorf("walk %v: %w", bucket, err)
|
|
}
|
|
|
|
return s3response.ListVersionsResult{
|
|
CommonPrefixes: results.CommonPrefixes,
|
|
DeleteMarkers: results.DelMarkers,
|
|
Delimiter: &delim,
|
|
IsTruncated: &results.Truncated,
|
|
KeyMarker: &keyMarker,
|
|
MaxKeys: input.MaxKeys,
|
|
Name: input.Bucket,
|
|
NextKeyMarker: &results.NextMarker,
|
|
NextVersionIdMarker: &results.NextVersionIdMarker,
|
|
Prefix: &prefix,
|
|
VersionIdMarker: &versionIdMarker,
|
|
Versions: results.ObjectVersions,
|
|
}, nil
|
|
}
|
|
|
|
func getBoolPtr(b bool) *bool {
|
|
return &b
|
|
}
|
|
|
|
// ensureNotDeleteMarker return a `MethodNotAllowd` error
|
|
// if the provided object(version) is a delete marker
|
|
func (p *Posix) ensureNotDeleteMarker(bucket, object, versionId string) error {
|
|
if !p.versioningEnabled() {
|
|
return nil
|
|
}
|
|
|
|
// With path-based metadata backends (e.g. sidecar), RetrieveAttribute
|
|
// returns ErrNoSuchKey whether the sidecar attribute is absent OR the
|
|
// data file simply doesn't exist — the two cases are indistinguishable
|
|
// from metadata alone. Verify the data file directly so callers
|
|
// receive the correct NoSuchVersion / NoSuchKey error.
|
|
if _, statErr := os.Stat(p.ObjectPath(bucket, object)); errors.Is(statErr, fs.ErrNotExist) || isErrNotDir(statErr) {
|
|
if versionId != "" {
|
|
return s3err.GetAPIError(s3err.ErrNoSuchVersion)
|
|
}
|
|
return s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
|
|
_, err := p.meta.RetrieveAttribute(nil, bucket, object, deleteMarkerKey)
|
|
if errors.Is(err, fs.ErrNotExist) || isErrNotDir(err) {
|
|
if versionId != "" {
|
|
return s3err.GetNoSuchVersionErr(object, versionId)
|
|
}
|
|
return s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
if errors.Is(err, meta.ErrNoSuchKey) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("get delete marker attr: %w", err)
|
|
}
|
|
|
|
return s3err.GetAPIError(s3err.ErrMethodNotAllowed)
|
|
}
|
|
|
|
// Check if the given object is a delete marker
|
|
func (p *Posix) isObjDeleteMarker(bucket, object string) (bool, error) {
|
|
_, err := p.meta.RetrieveAttribute(nil, bucket, object, deleteMarkerKey)
|
|
if errors.Is(err, fs.ErrNotExist) || isErrNotDir(err) {
|
|
return false, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
if errors.Is(err, meta.ErrNoSuchKey) {
|
|
return false, nil
|
|
}
|
|
if err != nil {
|
|
return false, fmt.Errorf("get object delete-marker: %w", err)
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// checkCopySourceDeleteMarker rejects a copy whose source resolves to a
|
|
// delete marker: the key has no current version when the marker is the
|
|
// latest, and a marker named by version id holds no data to copy.
|
|
func (p *Posix) checkCopySourceDeleteMarker(bucket, object, versionId string) error {
|
|
isDel, err := p.isObjDeleteMarker(bucket, object)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !isDel {
|
|
return nil
|
|
}
|
|
if versionId != "" {
|
|
return s3err.GetAPIError(s3err.ErrCopySourceDeleteMarker)
|
|
}
|
|
|
|
return s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
|
|
// Converts the file to object version. Finds all the object versions,
|
|
// delete markers from the versioning directory and returns
|
|
func (p *Posix) fileToObjVersions(bucket string) backend.GetVersionsFunc {
|
|
return func(path, versionIdMarker string, pastVersionIdMarker *bool, availableObjCount int, d fs.DirEntry) (*backend.ObjVersionFuncResult, error) {
|
|
var objects []s3response.ObjectVersion
|
|
var delMarkers []types.DeleteMarkerEntry
|
|
// if the number of available objects is 0, return truncated response
|
|
if availableObjCount <= 0 {
|
|
return &backend.ObjVersionFuncResult{
|
|
ObjectVersions: objects,
|
|
DelMarkers: delMarkers,
|
|
Truncated: true,
|
|
}, nil
|
|
}
|
|
// a directory is listed under the key with the trailing slash
|
|
key := path
|
|
if d.IsDir() {
|
|
key = path + "/"
|
|
}
|
|
|
|
// get object info and fill out object data
|
|
etagBytes, err := p.meta.RetrieveAttribute(nil, bucket, path, etagkey)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return nil, backend.ErrSkipObj
|
|
}
|
|
if d.IsDir() && errors.Is(err, meta.ErrNoSuchKey) {
|
|
// a directory is listed only if it's a directory object
|
|
return nil, backend.ErrSkipObj
|
|
}
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return nil, fmt.Errorf("get etag: %w", err)
|
|
}
|
|
// note: meta.ErrNoSuchKey will return etagBytes = []byte{}
|
|
// so this will just set etag to "" if its not already set
|
|
etag := string(etagBytes)
|
|
|
|
// If the object doesn't have versionId, it's 'null'
|
|
versionId := "null"
|
|
versionIdBytes, err := p.meta.RetrieveAttribute(nil, bucket, path, versionIdKey)
|
|
if err == nil {
|
|
versionId = string(versionIdBytes)
|
|
}
|
|
if versionId == versionIdMarker {
|
|
*pastVersionIdMarker = true
|
|
}
|
|
if *pastVersionIdMarker {
|
|
fi, err := d.Info()
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return nil, backend.ErrSkipObj
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get fileinfo: %w", err)
|
|
}
|
|
|
|
size := fi.Size()
|
|
if d.IsDir() {
|
|
// directory objects don't contain data
|
|
size = 0
|
|
}
|
|
|
|
isDel, err := p.isObjDeleteMarker(bucket, path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if isDel {
|
|
delMarkers = append(delMarkers, types.DeleteMarkerEntry{
|
|
IsLatest: getBoolPtr(true),
|
|
VersionId: &versionId,
|
|
LastModified: backend.GetTimePtr(fi.ModTime()),
|
|
Key: &key,
|
|
})
|
|
} else {
|
|
// Retrieve checksum
|
|
checksum, err := p.retrieveChecksums(nil, bucket, path)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return nil, fmt.Errorf("get checksum: %w", err)
|
|
}
|
|
|
|
objects = append(objects, s3response.ObjectVersion{
|
|
ETag: &etag,
|
|
Key: &key,
|
|
LastModified: backend.GetTimePtr(fi.ModTime()),
|
|
Size: &size,
|
|
VersionId: &versionId,
|
|
IsLatest: getBoolPtr(true),
|
|
StorageClass: types.ObjectVersionStorageClassStandard,
|
|
ChecksumAlgorithm: []types.ChecksumAlgorithm{checksum.Algorithm},
|
|
ChecksumType: checksum.Type,
|
|
})
|
|
}
|
|
|
|
availableObjCount--
|
|
if availableObjCount == 0 {
|
|
return &backend.ObjVersionFuncResult{
|
|
ObjectVersions: objects,
|
|
DelMarkers: delMarkers,
|
|
Truncated: true,
|
|
NextVersionIdMarker: versionId,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
if !p.versioningEnabled() {
|
|
return &backend.ObjVersionFuncResult{
|
|
ObjectVersions: objects,
|
|
DelMarkers: delMarkers,
|
|
}, nil
|
|
}
|
|
|
|
// List all the versions of the object in the versioning directory
|
|
versionPath := p.genObjVersionPath(bucket, key)
|
|
dirEnts, err := os.ReadDir(versionPath)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return &backend.ObjVersionFuncResult{
|
|
ObjectVersions: objects,
|
|
DelMarkers: delMarkers,
|
|
}, nil
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read version dir: %w", err)
|
|
}
|
|
|
|
if len(dirEnts) == 0 {
|
|
return &backend.ObjVersionFuncResult{
|
|
ObjectVersions: objects,
|
|
DelMarkers: delMarkers,
|
|
}, nil
|
|
}
|
|
|
|
// First find the null versionId object(if exists)
|
|
// before starting the object versions listing
|
|
var nullVersionIdObj *s3response.ObjectVersion
|
|
var nullObjDelMarker *types.DeleteMarkerEntry
|
|
nf, err := os.Stat(filepath.Join(versionPath, nullVersionId))
|
|
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
return nil, err
|
|
}
|
|
if err == nil {
|
|
isDel, err := p.isObjDeleteMarker(versionPath, nullVersionId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Check to see if the null versionId object is delete marker or not
|
|
if isDel {
|
|
nullObjDelMarker = &types.DeleteMarkerEntry{
|
|
VersionId: backend.GetPtrFromString("null"),
|
|
LastModified: backend.GetTimePtr(nf.ModTime()),
|
|
Key: &key,
|
|
IsLatest: getBoolPtr(false),
|
|
}
|
|
} else {
|
|
etagBytes, err := p.meta.RetrieveAttribute(nil, versionPath, nullVersionId, etagkey)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return nil, backend.ErrSkipObj
|
|
}
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return nil, fmt.Errorf("get etag: %w", err)
|
|
}
|
|
// note: meta.ErrNoSuchKey will return etagBytes = []byte{}
|
|
// so this will just set etag to "" if its not already set
|
|
etag := string(etagBytes)
|
|
size := nf.Size()
|
|
// Retrieve checksum
|
|
checksum, err := p.retrieveChecksums(nil, versionPath, nullVersionId)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return nil, fmt.Errorf("get checksum: %w", err)
|
|
}
|
|
|
|
nullVersionIdObj = &s3response.ObjectVersion{
|
|
ETag: &etag,
|
|
Key: &key,
|
|
LastModified: backend.GetTimePtr(nf.ModTime()),
|
|
Size: &size,
|
|
VersionId: backend.GetPtrFromString("null"),
|
|
IsLatest: getBoolPtr(false),
|
|
StorageClass: types.ObjectVersionStorageClassStandard,
|
|
ChecksumAlgorithm: []types.ChecksumAlgorithm{
|
|
checksum.Algorithm,
|
|
},
|
|
ChecksumType: checksum.Type,
|
|
}
|
|
}
|
|
}
|
|
|
|
isNullVersionIdObjFound := nullVersionIdObj != nil || nullObjDelMarker != nil
|
|
|
|
if len(dirEnts) == 1 && (isNullVersionIdObjFound) {
|
|
if nullObjDelMarker != nil {
|
|
delMarkers = append(delMarkers, *nullObjDelMarker)
|
|
}
|
|
if nullVersionIdObj != nil {
|
|
objects = append(objects, *nullVersionIdObj)
|
|
}
|
|
|
|
if availableObjCount == 1 {
|
|
return &backend.ObjVersionFuncResult{
|
|
ObjectVersions: objects,
|
|
DelMarkers: delMarkers,
|
|
Truncated: true,
|
|
NextVersionIdMarker: nullVersionId,
|
|
}, nil
|
|
} else {
|
|
return &backend.ObjVersionFuncResult{
|
|
ObjectVersions: objects,
|
|
DelMarkers: delMarkers,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
isNullVersionIdObjAdded := false
|
|
|
|
for i := len(dirEnts) - 1; i >= 0; i-- {
|
|
dEntry := dirEnts[i]
|
|
// Skip the null versionId object to not
|
|
// break the object versions list
|
|
if dEntry.Name() == nullVersionId {
|
|
continue
|
|
}
|
|
|
|
f, err := dEntry.Info()
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get fileinfo: %w", err)
|
|
}
|
|
|
|
// If the null versionId object is found, first push it
|
|
// by checking its creation date, then continue the adding
|
|
if isNullVersionIdObjFound && !isNullVersionIdObjAdded {
|
|
if nf.ModTime().After(f.ModTime()) {
|
|
if nullVersionIdObj != nil {
|
|
objects = append(objects, *nullVersionIdObj)
|
|
}
|
|
if nullObjDelMarker != nil {
|
|
delMarkers = append(delMarkers, *nullObjDelMarker)
|
|
}
|
|
|
|
isNullVersionIdObjAdded = true
|
|
|
|
if availableObjCount--; availableObjCount == 0 {
|
|
return &backend.ObjVersionFuncResult{
|
|
ObjectVersions: objects,
|
|
DelMarkers: delMarkers,
|
|
Truncated: true,
|
|
NextVersionIdMarker: nullVersionId,
|
|
}, nil
|
|
}
|
|
}
|
|
}
|
|
versionId := f.Name()
|
|
size := f.Size()
|
|
|
|
if !*pastVersionIdMarker {
|
|
if versionId == versionIdMarker {
|
|
*pastVersionIdMarker = true
|
|
}
|
|
continue
|
|
}
|
|
|
|
etagBytes, err := p.meta.RetrieveAttribute(nil, versionPath, versionId, etagkey)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return nil, backend.ErrSkipObj
|
|
}
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return nil, fmt.Errorf("get etag: %w", err)
|
|
}
|
|
// note: meta.ErrNoSuchKey will return etagBytes = []byte{}
|
|
// so this will just set etag to "" if its not already set
|
|
etag := string(etagBytes)
|
|
|
|
isDel, err := p.isObjDeleteMarker(versionPath, versionId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if isDel {
|
|
delMarkers = append(delMarkers, types.DeleteMarkerEntry{
|
|
VersionId: &versionId,
|
|
LastModified: backend.GetTimePtr(f.ModTime()),
|
|
Key: &key,
|
|
IsLatest: getBoolPtr(false),
|
|
})
|
|
} else {
|
|
// Retrieve checksum
|
|
checksum, err := p.retrieveChecksums(nil, versionPath, versionId)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return nil, fmt.Errorf("get checksum: %w", err)
|
|
}
|
|
objects = append(objects, s3response.ObjectVersion{
|
|
ETag: &etag,
|
|
Key: &key,
|
|
LastModified: backend.GetTimePtr(f.ModTime()),
|
|
Size: &size,
|
|
VersionId: &versionId,
|
|
IsLatest: getBoolPtr(false),
|
|
StorageClass: types.ObjectVersionStorageClassStandard,
|
|
ChecksumAlgorithm: []types.ChecksumAlgorithm{checksum.Algorithm},
|
|
ChecksumType: checksum.Type,
|
|
})
|
|
}
|
|
|
|
// if the available object count reaches to 0, return truncated response with nextVersionIdMarker
|
|
availableObjCount--
|
|
if availableObjCount == 0 {
|
|
return &backend.ObjVersionFuncResult{
|
|
ObjectVersions: objects,
|
|
DelMarkers: delMarkers,
|
|
Truncated: true,
|
|
NextVersionIdMarker: versionId,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
// If null versionId object is found but not yet pushed,
|
|
// push it after the listing, as it's the oldest object version
|
|
if isNullVersionIdObjFound && !isNullVersionIdObjAdded {
|
|
if nullVersionIdObj != nil {
|
|
objects = append(objects, *nullVersionIdObj)
|
|
}
|
|
if nullObjDelMarker != nil {
|
|
delMarkers = append(delMarkers, *nullObjDelMarker)
|
|
}
|
|
|
|
if availableObjCount--; availableObjCount == 0 {
|
|
return &backend.ObjVersionFuncResult{
|
|
ObjectVersions: objects,
|
|
DelMarkers: delMarkers,
|
|
Truncated: true,
|
|
NextVersionIdMarker: nullVersionId,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
return &backend.ObjVersionFuncResult{
|
|
ObjectVersions: objects,
|
|
DelMarkers: delMarkers,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
func (p *Posix) CreateMultipartUpload(ctx context.Context, mpu s3response.CreateMultipartUploadInput) (s3response.InitiateMultipartUploadResult, error) {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return s3response.InitiateMultipartUploadResult{}, err
|
|
}
|
|
defer release()
|
|
|
|
if mpu.Key == nil {
|
|
return s3response.InitiateMultipartUploadResult{}, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
|
|
bucket := *mpu.Bucket
|
|
object := *mpu.Key
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return s3response.InitiateMultipartUploadResult{}, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
|
|
err = p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return s3response.InitiateMultipartUploadResult{}, err
|
|
}
|
|
|
|
if strings.HasSuffix(*mpu.Key, "/") {
|
|
// directory objects can't be uploaded with multipart uploads
|
|
// because posix directories can't contain data
|
|
return s3response.InitiateMultipartUploadResult{}, s3err.GetAPIError(s3err.ErrDirectoryObjectContainsData)
|
|
}
|
|
|
|
// parse object tags
|
|
tags, err := backend.ParseObjectTags(getString(mpu.Tagging))
|
|
if err != nil {
|
|
return s3response.InitiateMultipartUploadResult{}, err
|
|
}
|
|
|
|
// generate random uuid for upload id
|
|
uploadID := uuid.New().String()
|
|
// hash object name for multipart container
|
|
objNameSum := sha256.Sum256([]byte(*mpu.Key))
|
|
// multiple uploads for same object name allowed,
|
|
// they will all go into the same hashed name directory
|
|
objdir := filepath.Join(MetaTmpMultipartDir, fmt.Sprintf("%x", objNameSum))
|
|
tmppath := p.ObjectPath(bucket, objdir)
|
|
// the unique upload id is a directory for all of the parts
|
|
// associated with this specific multipart upload
|
|
err = os.MkdirAll(filepath.Join(tmppath, uploadID), p.newDirPerm)
|
|
if err != nil {
|
|
return s3response.InitiateMultipartUploadResult{}, fmt.Errorf("create upload temp dir: %w", err)
|
|
}
|
|
|
|
// set an attribute with the original object name so that we can
|
|
// map the hashed name back to the original object name
|
|
err = p.meta.StoreAttribute(nil, bucket, objdir, onameAttr, []byte(object))
|
|
if err != nil {
|
|
// if we fail, cleanup the container directories
|
|
// but ignore errors because there might still be
|
|
// other uploads for the same object name outstanding
|
|
os.RemoveAll(filepath.Join(tmppath, uploadID))
|
|
os.Remove(tmppath)
|
|
return s3response.InitiateMultipartUploadResult{}, fmt.Errorf("set name attr for upload: %w", err)
|
|
}
|
|
|
|
// set object tagging
|
|
if tags != nil {
|
|
err := p.storeObjectTags(bucket, filepath.Join(objdir, uploadID), tags)
|
|
if err != nil {
|
|
// cleanup object if returning error
|
|
os.RemoveAll(filepath.Join(tmppath, uploadID))
|
|
os.Remove(tmppath)
|
|
_ = p.meta.DeleteAttributes(bucket, filepath.Join(objdir, uploadID))
|
|
return s3response.InitiateMultipartUploadResult{}, err
|
|
}
|
|
}
|
|
|
|
err = p.storeObjectMetaProperties(nil, bucket, filepath.Join(objdir, uploadID),
|
|
metaProperties{
|
|
ContentType: mpu.ContentType,
|
|
ContentEncoding: mpu.ContentEncoding,
|
|
ContentDisposition: mpu.ContentDisposition,
|
|
ContentLanguage: mpu.ContentLanguage,
|
|
CacheControl: mpu.CacheControl,
|
|
Expires: mpu.Expires,
|
|
WebsiteRedirectLocation: mpu.WebsiteRedirectLocation,
|
|
Metadata: mpu.Metadata,
|
|
})
|
|
if err != nil {
|
|
// cleanup object if returning error
|
|
os.RemoveAll(filepath.Join(tmppath, uploadID))
|
|
os.Remove(tmppath)
|
|
_ = p.meta.DeleteAttributes(bucket, filepath.Join(objdir, uploadID))
|
|
return s3response.InitiateMultipartUploadResult{}, err
|
|
}
|
|
|
|
// set object legal hold
|
|
if mpu.ObjectLockLegalHoldStatus == types.ObjectLockLegalHoldStatusOn {
|
|
err := p.isBucketObjectLockEnabled(bucket)
|
|
if err == nil {
|
|
err = p.meta.StoreAttribute(nil, bucket, filepath.Join(objdir, uploadID), objectLegalHoldKey, []byte{1})
|
|
}
|
|
if err != nil {
|
|
if errors.Is(err, s3err.GetAPIError(s3err.ErrMissingObjectLockConfiguration)) {
|
|
err = s3err.GetAPIError(s3err.ErrMissingObjectLockConfigurationNoSpaces)
|
|
}
|
|
// cleanup object if returning error
|
|
os.RemoveAll(filepath.Join(tmppath, uploadID))
|
|
os.Remove(tmppath)
|
|
_ = p.meta.DeleteAttributes(bucket, filepath.Join(objdir, uploadID))
|
|
return s3response.InitiateMultipartUploadResult{}, err
|
|
}
|
|
}
|
|
|
|
// Set object retention
|
|
if mpu.ObjectLockMode != "" {
|
|
retention := types.ObjectLockRetention{
|
|
Mode: types.ObjectLockRetentionMode(mpu.ObjectLockMode),
|
|
RetainUntilDate: mpu.ObjectLockRetainUntilDate,
|
|
}
|
|
retParsed, err := json.Marshal(retention)
|
|
if err != nil {
|
|
// cleanup object if returning error
|
|
os.RemoveAll(filepath.Join(tmppath, uploadID))
|
|
os.Remove(tmppath)
|
|
_ = p.meta.DeleteAttributes(bucket, filepath.Join(objdir, uploadID))
|
|
return s3response.InitiateMultipartUploadResult{}, fmt.Errorf("parse object lock retention: %w", err)
|
|
}
|
|
err = p.isBucketObjectLockEnabled(bucket)
|
|
if err == nil {
|
|
err = p.meta.StoreAttribute(nil, bucket, filepath.Join(objdir, uploadID), objectRetentionKey, retParsed)
|
|
}
|
|
if err != nil {
|
|
if errors.Is(err, s3err.GetAPIError(s3err.ErrMissingObjectLockConfiguration)) {
|
|
err = s3err.GetAPIError(s3err.ErrMissingObjectLockConfigurationNoSpaces)
|
|
}
|
|
// cleanup object if returning error
|
|
os.RemoveAll(filepath.Join(tmppath, uploadID))
|
|
os.Remove(tmppath)
|
|
_ = p.meta.DeleteAttributes(bucket, filepath.Join(objdir, uploadID))
|
|
return s3response.InitiateMultipartUploadResult{}, err
|
|
}
|
|
}
|
|
|
|
// Set object checksum algorithm
|
|
if mpu.ChecksumAlgorithm != "" {
|
|
err := p.storeChecksums(nil, bucket, filepath.Join(objdir, uploadID), s3response.Checksum{
|
|
Algorithm: mpu.ChecksumAlgorithm,
|
|
Type: mpu.ChecksumType,
|
|
})
|
|
if err != nil {
|
|
// cleanup object if returning error
|
|
_ = os.RemoveAll(filepath.Join(tmppath, uploadID))
|
|
_ = os.Remove(tmppath)
|
|
_ = p.meta.DeleteAttributes(bucket, filepath.Join(objdir, uploadID))
|
|
return s3response.InitiateMultipartUploadResult{}, fmt.Errorf("store mp checksum algorithm: %w", err)
|
|
}
|
|
}
|
|
|
|
return s3response.InitiateMultipartUploadResult{
|
|
Bucket: bucket,
|
|
Key: object,
|
|
UploadId: uploadID,
|
|
}, nil
|
|
}
|
|
|
|
// getChownIDs returns the uid and gid that should be used for chowning
|
|
// the object to the account uid/gid. It also returns a boolean indicating
|
|
// if chowning is needed.
|
|
func (p *Posix) getChownIDs(acct auth.Account) (int, int, bool) {
|
|
uid := p.euid
|
|
gid := p.egid
|
|
var needsChown bool
|
|
if p.chownuid && acct.UserID != p.euid {
|
|
uid = acct.UserID
|
|
needsChown = true
|
|
}
|
|
if p.chowngid && acct.GroupID != p.egid {
|
|
gid = acct.GroupID
|
|
needsChown = true
|
|
}
|
|
|
|
return uid, gid, needsChown
|
|
}
|
|
|
|
func getPartChecksum(algo types.ChecksumAlgorithm, part types.CompletedPart) string {
|
|
switch algo {
|
|
case types.ChecksumAlgorithmCrc32:
|
|
return backend.GetStringFromPtr(part.ChecksumCRC32)
|
|
case types.ChecksumAlgorithmCrc32c:
|
|
return backend.GetStringFromPtr(part.ChecksumCRC32C)
|
|
case types.ChecksumAlgorithmSha1:
|
|
return backend.GetStringFromPtr(part.ChecksumSHA1)
|
|
case types.ChecksumAlgorithmSha256:
|
|
return backend.GetStringFromPtr(part.ChecksumSHA256)
|
|
case types.ChecksumAlgorithmCrc64nvme:
|
|
return backend.GetStringFromPtr(part.ChecksumCRC64NVME)
|
|
case types.ChecksumAlgorithmSha512:
|
|
return backend.GetStringFromPtr(part.ChecksumSHA512)
|
|
case types.ChecksumAlgorithmMd5:
|
|
return backend.GetStringFromPtr(part.ChecksumMD5)
|
|
case types.ChecksumAlgorithmXxhash64:
|
|
return backend.GetStringFromPtr(part.ChecksumXXHASH64)
|
|
case types.ChecksumAlgorithmXxhash3:
|
|
return backend.GetStringFromPtr(part.ChecksumXXHASH3)
|
|
case types.ChecksumAlgorithmXxhash128:
|
|
return backend.GetStringFromPtr(part.ChecksumXXHASH128)
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func setUploadPartChecksum(res *s3.UploadPartOutput, algo types.ChecksumAlgorithm, sum *string) {
|
|
if sum == nil {
|
|
return
|
|
}
|
|
|
|
switch algo {
|
|
case types.ChecksumAlgorithmCrc32:
|
|
res.ChecksumCRC32 = sum
|
|
case types.ChecksumAlgorithmCrc32c:
|
|
res.ChecksumCRC32C = sum
|
|
case types.ChecksumAlgorithmSha1:
|
|
res.ChecksumSHA1 = sum
|
|
case types.ChecksumAlgorithmSha256:
|
|
res.ChecksumSHA256 = sum
|
|
case types.ChecksumAlgorithmCrc64nvme:
|
|
res.ChecksumCRC64NVME = sum
|
|
case types.ChecksumAlgorithmSha512:
|
|
res.ChecksumSHA512 = sum
|
|
case types.ChecksumAlgorithmMd5:
|
|
res.ChecksumMD5 = sum
|
|
case types.ChecksumAlgorithmXxhash64:
|
|
res.ChecksumXXHASH64 = sum
|
|
case types.ChecksumAlgorithmXxhash3:
|
|
res.ChecksumXXHASH3 = sum
|
|
case types.ChecksumAlgorithmXxhash128:
|
|
res.ChecksumXXHASH128 = sum
|
|
}
|
|
}
|
|
|
|
func (p *Posix) CompleteMultipartUpload(ctx context.Context, input *s3.CompleteMultipartUploadInput) (s3response.CompleteMultipartUploadResult, string, error) {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return s3response.CompleteMultipartUploadResult{}, "", err
|
|
}
|
|
defer release()
|
|
|
|
return p.CompleteMultipartUploadWithCopy(ctx, input, nil)
|
|
}
|
|
|
|
type onlyRead struct {
|
|
io.Reader
|
|
}
|
|
|
|
// CustomCopyFunc implements copying/moving data from one file descriptor
|
|
// to another. The bool return signifies if function is idempotent. If true
|
|
// the system will allow clients to retry CompleteMultipartUpload. If
|
|
// false, then once a copy function is called successfully the upload will
|
|
// be completely aborted on any subsequent failure.
|
|
type CustomCopyFunc func(from *os.File, to *os.File) (bool, error)
|
|
|
|
func (p *Posix) CompleteMultipartUploadWithCopy(ctx context.Context, input *s3.CompleteMultipartUploadInput, customCopy CustomCopyFunc) (s3response.CompleteMultipartUploadResult, string, error) {
|
|
acct, ok := ctx.Value("account").(auth.Account)
|
|
if !ok {
|
|
acct = auth.Account{}
|
|
}
|
|
|
|
res := s3response.CompleteMultipartUploadResult{}
|
|
|
|
if input.Key == nil {
|
|
return res, "", s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
if input.UploadId == nil {
|
|
return res, "", s3err.GetNoSuchUploadErr("")
|
|
}
|
|
if input.MultipartUpload == nil {
|
|
return res, "", s3err.GetAPIError(s3err.ErrInvalidRequest)
|
|
}
|
|
|
|
bucket := *input.Bucket
|
|
object := *input.Key
|
|
uploadID := *input.UploadId
|
|
parts := input.MultipartUpload.Parts
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return res, "", s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
|
|
err := p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return res, "", err
|
|
}
|
|
|
|
// Rename the upload directory to <uploadId><ETag> to atomically claim
|
|
// the processing slot. A concurrent call with the same uploadId will compute
|
|
// the same ETag, so it will either find the directory still present (still
|
|
// processing) or gone (already completed) and react accordingly.
|
|
sum := sha256.Sum256([]byte(object))
|
|
objdirFull := filepath.Join(p.BucketPath(bucket), MetaTmpMultipartDir, fmt.Sprintf("%x", sum))
|
|
uploadIDDir := filepath.Join(objdirFull, uploadID)
|
|
// Compute the default multipart ETag token used for claim naming.
|
|
// In standard mode this is the S3-compatible multipart MD5 ETag; in
|
|
// dataIntegrityEtag mode it may fall back to a deterministic claim token.
|
|
multipartClaimToken, err := backend.ComputeMultipartETagFromPartETags(parts)
|
|
if err != nil {
|
|
if !p.dataIntegrityEtag {
|
|
return res, "", err
|
|
}
|
|
|
|
// dataIntegrityEtag allows non-MD5 multipart part ETags (for example,
|
|
// "CRC64NVME-<checksum>"). In that mode, ComputeMultipartETagFromPartETags
|
|
// cannot decode part ETags as hex MD5 bytes. Build a deterministic
|
|
// claim token from part-number/etag pairs so concurrent complete calls
|
|
// still contend on the same in-progress directory name.
|
|
h := sha256.New()
|
|
for _, part := range parts {
|
|
if part.ETag == nil || part.PartNumber == nil {
|
|
return res, "", s3err.GetAPIError(s3err.ErrMalformedXML)
|
|
}
|
|
_, _ = h.Write([]byte(strconv.FormatInt(int64(*part.PartNumber), 10)))
|
|
_, _ = h.Write([]byte{':'})
|
|
_, _ = h.Write([]byte(strings.Trim(*part.ETag, "\"")))
|
|
_, _ = h.Write([]byte{';'})
|
|
}
|
|
multipartClaimToken = fmt.Sprintf("\"%x-%d\"", h.Sum(nil), len(parts))
|
|
}
|
|
activeUploadName := fmt.Sprintf("%s.%s%s", uploadID, strings.Trim(multipartClaimToken, "\""), inProgressSuffix)
|
|
uploadIDInProgress := filepath.Join(objdirFull, activeUploadName)
|
|
objdir := filepath.Join(MetaTmpMultipartDir, fmt.Sprintf("%x", sum))
|
|
|
|
predictDataIntegrityFinalETag := func(uploadName string) (string, error) {
|
|
checksums, err := p.retrieveChecksums(nil, bucket, filepath.Join(objdir, uploadName))
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return "", fmt.Errorf("get mp checksums: %w", err)
|
|
}
|
|
|
|
mpChecksumType := checksums.Type
|
|
if checksums.Type == "" {
|
|
checksums.Type = types.ChecksumTypeFullObject
|
|
checksums.Algorithm = types.ChecksumAlgorithmCrc64nvme
|
|
}
|
|
|
|
var compositeChecksumRdr *utils.CompositeChecksumReader
|
|
if checksums.Type == types.ChecksumTypeComposite {
|
|
compositeChecksumRdr, err = utils.NewCompositeChecksumReader(utils.HashType(strings.ToLower(string(checksums.Algorithm))))
|
|
if err != nil {
|
|
return "", fmt.Errorf("initialize composite checksum reader: %w", err)
|
|
}
|
|
}
|
|
|
|
var value string
|
|
var composableCsum string
|
|
for i, part := range parts {
|
|
if part.PartNumber == nil || part.ETag == nil {
|
|
return "", s3err.GetAPIError(s3err.ErrMalformedXML)
|
|
}
|
|
|
|
partObjPath := filepath.Join(objdir, uploadName, fmt.Sprintf("%v", *part.PartNumber))
|
|
fi, err := os.Lstat(p.ObjectPath(bucket, partObjPath))
|
|
if err != nil {
|
|
return "", s3err.GetInvalidPartErr(uploadID, *part.PartNumber, backend.GetStringFromPtr(part.ETag))
|
|
}
|
|
|
|
switch checksums.Type {
|
|
case types.ChecksumTypeFullObject:
|
|
var pcs string
|
|
if mpChecksumType != "" {
|
|
pcs = getPartChecksum(checksums.Algorithm, part)
|
|
} else {
|
|
crc64nvme, err := p.meta.RetrieveAttribute(nil, bucket, partObjPath, partCrc64nvme)
|
|
if err != nil {
|
|
return "", fmt.Errorf("retrieve part internal crc64nvme: %w", err)
|
|
}
|
|
pcs = string(crc64nvme)
|
|
}
|
|
|
|
if i == 0 {
|
|
composableCsum = pcs
|
|
} else {
|
|
composableCsum, err = utils.AddCRCChecksum(checksums.Algorithm, composableCsum, pcs, fi.Size())
|
|
if err != nil {
|
|
return "", fmt.Errorf("add part %v checksum: %w", *part.PartNumber, err)
|
|
}
|
|
}
|
|
case types.ChecksumTypeComposite:
|
|
if err := compositeChecksumRdr.Process(getPartChecksum(checksums.Algorithm, part)); err != nil {
|
|
return "", fmt.Errorf("process %v part checksum: %w", *part.PartNumber, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
switch checksums.Type {
|
|
case types.ChecksumTypeComposite:
|
|
value = fmt.Sprintf("%s-%v", compositeChecksumRdr.Sum(), len(parts))
|
|
case types.ChecksumTypeFullObject:
|
|
value = composableCsum
|
|
}
|
|
|
|
if checksums.Algorithm == "" || value == "" {
|
|
return multipartClaimToken, nil
|
|
}
|
|
return fmt.Sprintf("\"%s-%s\"", strings.ToUpper(string(checksums.Algorithm)), value), nil
|
|
}
|
|
|
|
err = os.Rename(uploadIDDir, uploadIDInProgress)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
// Another call already claimed this slot and is still assembling the object.
|
|
if _, statErr := os.Stat(uploadIDInProgress); statErr == nil {
|
|
etag := multipartClaimToken
|
|
if p.dataIntegrityEtag {
|
|
etag, err = predictDataIntegrityFinalETag(activeUploadName)
|
|
if err != nil {
|
|
return res, "", err
|
|
}
|
|
}
|
|
// Still in progress — treat as success for idempotency.
|
|
return s3response.CompleteMultipartUploadResult{
|
|
Bucket: &bucket,
|
|
ETag: &etag,
|
|
Key: &object,
|
|
}, "", nil
|
|
}
|
|
// Directory is gone: the concurrent call already completed and cleaned up.
|
|
// A directory at the object path is the object of the key with a
|
|
// trailing slash, not the completed upload.
|
|
if fi, statErr := os.Stat(p.ObjectPath(bucket, object)); statErr == nil && !fi.IsDir() {
|
|
etag := multipartClaimToken
|
|
if p.dataIntegrityEtag {
|
|
etagBytes, etagErr := p.meta.RetrieveAttribute(nil, bucket, object, etagkey)
|
|
if etagErr != nil {
|
|
return res, "", fmt.Errorf("get object etag: %w", etagErr)
|
|
}
|
|
etag = string(etagBytes)
|
|
}
|
|
return s3response.CompleteMultipartUploadResult{
|
|
Bucket: &bucket,
|
|
ETag: &etag,
|
|
Key: &object,
|
|
}, "", nil
|
|
}
|
|
|
|
// Last resort: the object stat above may have lost a race with the
|
|
// concurrent call's link step. Check the mp-metadata xattr, as this
|
|
// multipart upload may have been finalized and the final object has been created
|
|
// before or by the racing request
|
|
if mpMetaBytes, statErr := p.meta.RetrieveAttribute(nil, bucket, object, mpMetaKey); statErr == nil {
|
|
mpMeta, err := backend.UnmarshalMpUploadMetadata(mpMetaBytes, false)
|
|
if err != nil {
|
|
return res, "", fmt.Errorf("parse object multipart metadata: %w", err)
|
|
}
|
|
|
|
// The object may have been overwritten by a newer upload or
|
|
// it's the result of a completely different multipart upload; only
|
|
// treat it as our completion if the upload IDs match.
|
|
if mpMeta.UploadID != uploadID {
|
|
return res, "", s3err.GetAPIError(s3err.ErrNoSuchUpload)
|
|
}
|
|
|
|
etag := multipartClaimToken
|
|
if p.dataIntegrityEtag {
|
|
etagBytes, etagErr := p.meta.RetrieveAttribute(nil, bucket, object, etagkey)
|
|
if etagErr != nil {
|
|
return res, "", fmt.Errorf("get object etag: %w", etagErr)
|
|
}
|
|
etag = string(etagBytes)
|
|
}
|
|
return s3response.CompleteMultipartUploadResult{
|
|
Bucket: &bucket,
|
|
ETag: &etag,
|
|
Key: &object,
|
|
}, "", nil
|
|
}
|
|
|
|
return res, "", s3err.GetAPIError(s3err.ErrNoSuchUpload)
|
|
}
|
|
if err != nil {
|
|
return res, "", fmt.Errorf("rename upload to etag dir: %w", err)
|
|
}
|
|
|
|
// Rename sidecar metadata to match the new data directory path.
|
|
// For xattr this is a no-op since attributes follow the inode.
|
|
metaObjDir := filepath.Join(MetaTmpMultipartDir, fmt.Sprintf("%x", sum))
|
|
oldMetaObj := filepath.Join(metaObjDir, uploadID)
|
|
newMetaObj := filepath.Join(metaObjDir, activeUploadName)
|
|
if err := p.meta.RenameObject(bucket, oldMetaObj, newMetaObj); err != nil {
|
|
// Roll back the data directory rename so a future retry can succeed.
|
|
os.Rename(uploadIDInProgress, uploadIDDir)
|
|
return res, "", fmt.Errorf("rename metadata for in-progress: %w", err)
|
|
}
|
|
|
|
// Best-effort rename back on failure so a future retry can still complete.
|
|
// On success, os.RemoveAll below removes uploadIDInProgress so this is a no-op.
|
|
defer os.Rename(uploadIDInProgress, uploadIDDir)
|
|
defer p.meta.RenameObject(bucket, newMetaObj, oldMetaObj)
|
|
|
|
// Fast-fail precondition and directory checks before the parts are
|
|
// assembled. These are only advisory: the authoritative checks are
|
|
// repeated while holding the object publish lock just before the final
|
|
// link.
|
|
err = p.checkPutPreconditions(bucket, object, input.IfMatch, input.IfNoneMatch)
|
|
if err != nil {
|
|
return res, "", err
|
|
}
|
|
if d, err := os.Stat(p.ObjectPath(bucket, object)); err == nil && d.IsDir() {
|
|
return res, "", s3err.GetAPIError(s3err.ErrExistingObjectIsDirectory)
|
|
}
|
|
|
|
checksums, err := p.retrieveChecksums(nil, bucket, filepath.Join(objdir, activeUploadName))
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return res, "", fmt.Errorf("get mp checksums: %w", err)
|
|
}
|
|
|
|
// ChecksumType should be the same as specified on CreateMultipartUpload
|
|
if input.ChecksumType != "" && checksums.Type != input.ChecksumType {
|
|
checksumType := checksums.Type
|
|
if checksumType == "" {
|
|
checksumType = types.ChecksumType("null")
|
|
}
|
|
|
|
return res, "", s3err.GetChecksumTypeMismatchOnMpErr(checksumType)
|
|
}
|
|
|
|
// mpChecksumType holds the multipart upload checksum type
|
|
mpChecksumType := checksums.Type
|
|
|
|
// The checksum type/algorithm should default to FULL_OBJECT(crc64nvme)
|
|
if checksums.Type == "" {
|
|
checksums.Type = types.ChecksumTypeFullObject
|
|
checksums.Algorithm = types.ChecksumAlgorithmCrc64nvme
|
|
}
|
|
|
|
// Initialize composite checksum reader
|
|
var compositeChecksumRdr *utils.CompositeChecksumReader
|
|
if checksums.Type == types.ChecksumTypeComposite {
|
|
compositeChecksumRdr, err = utils.NewCompositeChecksumReader(utils.HashType(strings.ToLower(string(checksums.Algorithm))))
|
|
if err != nil {
|
|
return res, "", fmt.Errorf("initialize composite checksum reader: %w", err)
|
|
}
|
|
}
|
|
|
|
// check all parts ok
|
|
last := len(parts) - 1
|
|
var totalsize int64
|
|
// cumulative byte offsets: partSizes[i] = sum of sizes of parts 1..i+1
|
|
var partSizes []int64
|
|
var composableCsum string
|
|
|
|
// The initial value is the lower limit of partNumber: 0
|
|
var partNumber int32
|
|
for i, part := range parts {
|
|
if part.PartNumber == nil {
|
|
return res, "", s3err.GetAPIError(s3err.ErrMalformedXML)
|
|
}
|
|
if *part.PartNumber < 1 {
|
|
return res, "", s3err.GetInvalidArgumentErr(s3err.InvalidArgCompleteMpPartNumber, fmt.Sprint(*part.PartNumber))
|
|
}
|
|
if *part.PartNumber <= partNumber {
|
|
return res, "", s3err.GetAPIError(s3err.ErrInvalidPartOrder)
|
|
}
|
|
|
|
partNumber = *part.PartNumber
|
|
|
|
partObjPath := filepath.Join(objdir, activeUploadName, fmt.Sprintf("%v", *part.PartNumber))
|
|
fullPartPath := p.ObjectPath(bucket, partObjPath)
|
|
fi, err := os.Lstat(fullPartPath)
|
|
if err != nil {
|
|
return res, "", s3err.GetInvalidPartErr(uploadID, *part.PartNumber, backend.GetStringFromPtr(part.ETag))
|
|
}
|
|
|
|
totalsize += fi.Size()
|
|
partSizes = append(partSizes, totalsize)
|
|
// all parts except the last need to be greater, than or equal to
|
|
// the minimum allowed size (5 Mib)
|
|
if i < last && fi.Size() < backend.MinPartSize {
|
|
return res, "", s3err.GetEntityTooSmallErr(fi.Size(), backend.MinPartSize)
|
|
}
|
|
|
|
b, err := p.meta.RetrieveAttribute(nil, bucket, partObjPath, etagkey)
|
|
etag := string(b)
|
|
if err != nil {
|
|
etag = ""
|
|
}
|
|
if parts[i].ETag == nil {
|
|
return res, "", s3err.GetAPIError(s3err.ErrMalformedXML)
|
|
}
|
|
if !backend.AreEtagsSame(etag, *parts[i].ETag) {
|
|
return res, "", s3err.GetInvalidPartErr(uploadID, *part.PartNumber, etag)
|
|
}
|
|
|
|
partChecksum, err := p.retrieveChecksums(nil, bucket, partObjPath)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return res, "", fmt.Errorf("get part checksum: %w", err)
|
|
}
|
|
|
|
// If checksum has been provided on mp initialization
|
|
err = validatePartChecksum(partChecksum, part, uploadID)
|
|
if err != nil {
|
|
return res, "", err
|
|
}
|
|
|
|
// Accumulate checksum state.
|
|
switch checksums.Type {
|
|
case types.ChecksumTypeFullObject:
|
|
var pcs string
|
|
if mpChecksumType != "" {
|
|
pcs = getPartChecksum(checksums.Algorithm, part)
|
|
} else {
|
|
crc64nvme, err := p.meta.RetrieveAttribute(nil, bucket, partObjPath, partCrc64nvme)
|
|
if err != nil {
|
|
return res, "", fmt.Errorf("retrieve part internal crc64nvme: %w", err)
|
|
}
|
|
pcs = string(crc64nvme)
|
|
}
|
|
if i == 0 {
|
|
composableCsum = pcs
|
|
} else {
|
|
composableCsum, err = utils.AddCRCChecksum(checksums.Algorithm, composableCsum, pcs, fi.Size())
|
|
if err != nil {
|
|
return res, "", fmt.Errorf("add part %v checksum: %w", *part.PartNumber, err)
|
|
}
|
|
}
|
|
case types.ChecksumTypeComposite:
|
|
if err := compositeChecksumRdr.Process(getPartChecksum(checksums.Algorithm, part)); err != nil {
|
|
return res, "", fmt.Errorf("process %v part checksum: %w", *part.PartNumber, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if input.MpuObjectSize != nil && totalsize != *input.MpuObjectSize {
|
|
return res, "", s3err.GetIncorrectMpObjectSizeErr(totalsize, *input.MpuObjectSize)
|
|
}
|
|
|
|
// Compute the final checksum value.
|
|
var value string
|
|
switch checksums.Type {
|
|
case types.ChecksumTypeComposite:
|
|
value = fmt.Sprintf("%s-%v", compositeChecksumRdr.Sum(), len(parts))
|
|
case types.ChecksumTypeFullObject:
|
|
value = composableCsum
|
|
}
|
|
|
|
var crc32 *string
|
|
var crc32c *string
|
|
var sha1 *string
|
|
var sha256 *string
|
|
var crc64nvme *string
|
|
var sha512 *string
|
|
var md5sum *string
|
|
var xxhash64 *string
|
|
var xxhash3 *string
|
|
var xxhash128 *string
|
|
var gotSum *string
|
|
|
|
switch checksums.Algorithm {
|
|
case types.ChecksumAlgorithmCrc32:
|
|
gotSum = input.ChecksumCRC32
|
|
checksums.CRC32 = &value
|
|
crc32 = &value
|
|
case types.ChecksumAlgorithmCrc32c:
|
|
gotSum = input.ChecksumCRC32C
|
|
checksums.CRC32C = &value
|
|
crc32c = &value
|
|
case types.ChecksumAlgorithmSha1:
|
|
gotSum = input.ChecksumSHA1
|
|
checksums.SHA1 = &value
|
|
sha1 = &value
|
|
case types.ChecksumAlgorithmSha256:
|
|
gotSum = input.ChecksumSHA256
|
|
checksums.SHA256 = &value
|
|
sha256 = &value
|
|
case types.ChecksumAlgorithmCrc64nvme:
|
|
gotSum = input.ChecksumCRC64NVME
|
|
checksums.CRC64NVME = &value
|
|
crc64nvme = &value
|
|
case types.ChecksumAlgorithmSha512:
|
|
gotSum = input.ChecksumSHA512
|
|
checksums.SHA512 = &value
|
|
sha512 = &value
|
|
case types.ChecksumAlgorithmMd5:
|
|
gotSum = input.ChecksumMD5
|
|
checksums.MD5 = &value
|
|
md5sum = &value
|
|
case types.ChecksumAlgorithmXxhash64:
|
|
gotSum = input.ChecksumXXHASH64
|
|
checksums.XXHASH64 = &value
|
|
xxhash64 = &value
|
|
case types.ChecksumAlgorithmXxhash3:
|
|
gotSum = input.ChecksumXXHASH3
|
|
checksums.XXHASH3 = &value
|
|
xxhash3 = &value
|
|
case types.ChecksumAlgorithmXxhash128:
|
|
gotSum = input.ChecksumXXHASH128
|
|
checksums.XXHASH128 = &value
|
|
xxhash128 = &value
|
|
}
|
|
|
|
// Check if the provided checksum and the calculated one are the same.
|
|
if mpChecksumType != "" && gotSum != nil {
|
|
s := *gotSum
|
|
if checksums.Type == types.ChecksumTypeComposite && !strings.Contains(s, "-") {
|
|
s = fmt.Sprintf("%s-%v", s, len(parts))
|
|
}
|
|
if s != value {
|
|
return res, "", s3err.GetChecksumBadDigestErr(checksums.Algorithm)
|
|
}
|
|
}
|
|
|
|
// Determine the ETag that will be stored on the final object and returned
|
|
// to the client. By default this is the S3-compatible MD5-of-part-ETags
|
|
// value. When dataIntegrityEtag is enabled, embed the checksum algorithm
|
|
// and computed value instead:
|
|
// CRC64NVME full-object: "CRC64NVME-<whole-file-checksum>"
|
|
// composite other algo: "ALGO-<composite-checksum>-<part-count>"
|
|
// full-object other algo: "ALGO-<whole-file-checksum>"
|
|
finalEtag := multipartClaimToken
|
|
if p.dataIntegrityEtag && checksums.Algorithm != "" && value != "" {
|
|
finalEtag = fmt.Sprintf("\"%s-%s\"", strings.ToUpper(string(checksums.Algorithm)), value)
|
|
}
|
|
|
|
f, err := p.openTmpFile(p.ObjectPath(bucket, MetaTmpDir), bucket, object,
|
|
totalsize, acct, skipFalloc, p.forceNoTmpFile, odirectNotAllowed)
|
|
if err != nil {
|
|
if errors.Is(err, syscall.EDQUOT) {
|
|
return res, "", s3err.GetAPIError(s3err.ErrQuotaExceeded)
|
|
}
|
|
if errors.Is(err, syscall.ENOSPC) {
|
|
return res, "", s3err.GetAPIError(s3err.ErrNoSpaceLeftOnDevice)
|
|
}
|
|
return res, "", fmt.Errorf("open temp file: %w", err)
|
|
}
|
|
defer f.cleanup()
|
|
|
|
var abortOnErrSet bool
|
|
for _, part := range parts {
|
|
partObjPath := filepath.Join(objdir, activeUploadName, fmt.Sprintf("%v", *part.PartNumber))
|
|
fullPartPath := p.ObjectPath(bucket, partObjPath)
|
|
pf, err := os.Open(fullPartPath)
|
|
if err != nil {
|
|
return res, "", fmt.Errorf("open part %v: %v", *part.PartNumber, err)
|
|
}
|
|
|
|
if customCopy != nil {
|
|
idemp, err := customCopy(pf, f.File())
|
|
if err != nil {
|
|
// Fail back to standard copy
|
|
debuglogger.Logf("custom data block move failed (%q/%q): %v, failing back to io.Copy()",
|
|
bucket, object, err)
|
|
_, _ = f.File().Seek(0, io.SeekEnd)
|
|
if p.forceNoCopyFileRange {
|
|
_, err = io.Copy(f, &onlyRead{pf})
|
|
} else {
|
|
// Keep both endpoints as *os.File here so
|
|
// io.Copy can use copy_file_range.
|
|
_, err = io.Copy(f.File(), pf)
|
|
}
|
|
}
|
|
if !idemp && err == nil {
|
|
// a successful non-idempotent call means we can no longer
|
|
// retry this upload. any failure needs to abort the complete
|
|
// upload.
|
|
if !abortOnErrSet {
|
|
defer func() {
|
|
// cleanup tmp dirs
|
|
os.RemoveAll(filepath.Join(p.BucketPath(bucket), objdir, activeUploadName))
|
|
// use Remove for objdir in case there are still other
|
|
// uploads for same object name outstanding, this will
|
|
// fail if there are any
|
|
os.Remove(p.ObjectPath(bucket, objdir))
|
|
}()
|
|
}
|
|
abortOnErrSet = true
|
|
}
|
|
} else {
|
|
if p.forceNoCopyFileRange {
|
|
_, err = io.Copy(f, &onlyRead{pf})
|
|
} else {
|
|
// Keep both endpoints as *os.File here so
|
|
// io.Copy can use copy_file_range.
|
|
_, err = io.Copy(f.File(), pf)
|
|
}
|
|
}
|
|
pf.Close()
|
|
if err != nil {
|
|
if errors.Is(err, syscall.EDQUOT) {
|
|
return res, "", s3err.GetAPIError(s3err.ErrQuotaExceeded)
|
|
}
|
|
if errors.Is(err, syscall.ENOSPC) {
|
|
return res, "", s3err.GetAPIError(s3err.ErrNoSpaceLeftOnDevice)
|
|
}
|
|
return res, "", fmt.Errorf("copy part %v: %v", part.PartNumber, err)
|
|
}
|
|
}
|
|
|
|
// The parts are fully assembled into the staging file; serialize the
|
|
// commit phase (condition evaluation, metadata stores, and final link)
|
|
// with competing writers to the same key.
|
|
unlock, err := p.lockObjectPublish(ctx, bucket, object)
|
|
if err != nil {
|
|
return res, "", err
|
|
}
|
|
defer unlock()
|
|
|
|
// authoritative precondition check under the publish lock
|
|
err = p.checkPutPreconditions(bucket, object, input.IfMatch, input.IfNoneMatch)
|
|
if err != nil {
|
|
return res, "", err
|
|
}
|
|
|
|
upiddir := filepath.Join(objdir, activeUploadName)
|
|
|
|
objMeta := p.loadObjectMetaProperties(nil, bucket, upiddir, nil)
|
|
err = p.storeObjectMetaProperties(f.File(), bucket, object, objMeta)
|
|
if err != nil {
|
|
return res, "", err
|
|
}
|
|
|
|
objname := p.ObjectPath(bucket, object)
|
|
dir := filepath.Dir(objname)
|
|
if dir != "" {
|
|
uid, gid, doChown := p.getChownIDs(acct)
|
|
err = p.mkdirAll(dir, uid, gid, doChown)
|
|
if err != nil {
|
|
return res, "", err
|
|
}
|
|
}
|
|
|
|
vStatus, err := p.getBucketVersioningStatus(ctx, bucket)
|
|
if err != nil {
|
|
return res, "", err
|
|
}
|
|
vEnabled := p.isBucketVersioningEnabled(vStatus)
|
|
|
|
d, err := os.Stat(objname)
|
|
if err == nil && d.IsDir() {
|
|
// the directory is the object of the key with a trailing slash, or
|
|
// the parent of other objects: its attributes are not this object's
|
|
return res, "", s3err.GetAPIError(s3err.ErrExistingObjectIsDirectory)
|
|
}
|
|
|
|
// if the versioning is enabled first create the file object version
|
|
if p.versioningEnabled() && vEnabled && err == nil {
|
|
_, err := p.createObjVersion(bucket, object, d.Size(), acct, false)
|
|
if err != nil {
|
|
return res, "", fmt.Errorf("create object version: %w", err)
|
|
}
|
|
// Clean up object-lock attrs that may have leaked from the previous
|
|
// version's path-based metadata into this (new) version's sidecar.
|
|
_ = p.meta.DeleteAttribute(bucket, object, objectLegalHoldKey)
|
|
_ = p.meta.DeleteAttribute(bucket, object, objectRetentionKey)
|
|
}
|
|
|
|
// Clear the live marker after any snapshot, including when the data file
|
|
// is missing or versioning does not require archiving the previous object.
|
|
err = p.meta.DeleteAttribute(bucket, object, deleteMarkerKey)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) && !errors.Is(err, fs.ErrNotExist) {
|
|
return res, "", fmt.Errorf("delete object delete-marker: %w", err)
|
|
}
|
|
|
|
// if the versioning is enabled, generate a new versionID for the object
|
|
var versionID string
|
|
if p.versioningEnabled() && vEnabled {
|
|
versionID = ulid.Make().String()
|
|
|
|
err := p.meta.StoreAttribute(f.File(), bucket, object, versionIdKey, []byte(versionID))
|
|
if err != nil {
|
|
return res, "", fmt.Errorf("set versionId attr: %w", err)
|
|
}
|
|
}
|
|
|
|
// load and set tagging
|
|
tagging, err := p.meta.RetrieveAttribute(nil, bucket, upiddir, tagHdr)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return res, "", fmt.Errorf("get object tagging: %w", err)
|
|
}
|
|
if err == nil {
|
|
err := p.meta.StoreAttribute(f.File(), bucket, object, tagHdr, tagging)
|
|
if err != nil {
|
|
return res, "", fmt.Errorf("set object tagging: %w", err)
|
|
}
|
|
}
|
|
|
|
// load and set legal hold
|
|
lHold, err := p.meta.RetrieveAttribute(nil, bucket, upiddir, objectLegalHoldKey)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return res, "", fmt.Errorf("get object legal hold: %w", err)
|
|
}
|
|
if err == nil {
|
|
err := p.meta.StoreAttribute(f.File(), bucket, object, objectLegalHoldKey, lHold)
|
|
if err != nil {
|
|
return res, "", fmt.Errorf("set object legal hold: %w", err)
|
|
}
|
|
}
|
|
|
|
err = p.storeChecksums(f.File(), bucket, object, checksums)
|
|
if err != nil {
|
|
return res, "", fmt.Errorf("store object checksum: %w", err)
|
|
}
|
|
|
|
// load and set retention
|
|
ret, err := p.meta.RetrieveAttribute(nil, bucket, upiddir, objectRetentionKey)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return res, "", fmt.Errorf("get object retention: %w", err)
|
|
}
|
|
if err == nil {
|
|
err := p.meta.StoreAttribute(f.File(), bucket, object, objectRetentionKey, ret)
|
|
if err != nil {
|
|
return res, "", fmt.Errorf("set object retention: %w", err)
|
|
}
|
|
}
|
|
|
|
err = p.meta.StoreAttribute(f.File(), bucket, object, etagkey, []byte(finalEtag))
|
|
if err != nil {
|
|
return res, "", fmt.Errorf("set etag attr: %w", err)
|
|
}
|
|
|
|
// Store multipart upload metadata on the final object so that GetObject /
|
|
// HeadObject can serve individual parts by part-number.
|
|
mpMeta := backend.MpUploadMetadata{UploadID: uploadID, Parts: partSizes}
|
|
mpMetaBytes, err := backend.MarshalMpUploadMetadata(mpMeta, false)
|
|
if err != nil {
|
|
return res, "", fmt.Errorf("marshal object multipart metadata: %w", err)
|
|
}
|
|
|
|
err = p.meta.StoreAttribute(f.File(), bucket, object, mpMetaKey, mpMetaBytes)
|
|
if err != nil {
|
|
return res, "", fmt.Errorf("set object multipart metadata: %w", err)
|
|
}
|
|
|
|
err = f.link()
|
|
if err != nil {
|
|
return res, "", fmt.Errorf("link object in namespace: %w", err)
|
|
}
|
|
|
|
// cleanup tmp dirs
|
|
os.RemoveAll(filepath.Join(p.BucketPath(bucket), objdir, activeUploadName))
|
|
// use Remove for objdir in case there are still other uploads
|
|
// for same object name outstanding, this will fail if there are any
|
|
os.Remove(p.ObjectPath(bucket, objdir))
|
|
|
|
return s3response.CompleteMultipartUploadResult{
|
|
Bucket: &bucket,
|
|
ETag: &finalEtag,
|
|
Key: &object,
|
|
ChecksumCRC32: crc32,
|
|
ChecksumCRC32C: crc32c,
|
|
ChecksumSHA1: sha1,
|
|
ChecksumSHA256: sha256,
|
|
ChecksumCRC64NVME: crc64nvme,
|
|
ChecksumSHA512: sha512,
|
|
ChecksumMD5: md5sum,
|
|
ChecksumXXHASH64: xxhash64,
|
|
ChecksumXXHASH3: xxhash3,
|
|
ChecksumXXHASH128: xxhash128,
|
|
ChecksumType: &checksums.Type,
|
|
}, versionID, nil
|
|
}
|
|
|
|
func validatePartChecksum(checksum s3response.Checksum, part types.CompletedPart, uploadId string) error {
|
|
n, argValue := numberOfChecksums(part)
|
|
if n > 1 {
|
|
return s3err.GetInvalidArgumentErr(s3err.InvalidArgChecksumPart, argValue)
|
|
}
|
|
if checksum.Algorithm == "" {
|
|
if n != 0 {
|
|
return s3err.GetInvalidPartErr(uploadId, *part.PartNumber, *part.ETag)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
algo := checksum.Algorithm
|
|
if n == 0 {
|
|
return s3err.APIError{
|
|
Code: "InvalidRequest",
|
|
Description: fmt.Sprintf("The upload was created using a %v checksum. The complete request must include the checksum for each part. It was missing for part %v in the request.", strings.ToLower(string(algo)), *part.PartNumber),
|
|
HTTPStatusCode: http.StatusBadRequest,
|
|
}
|
|
}
|
|
|
|
for _, cs := range []struct {
|
|
checksum *string
|
|
expectedChecksum string
|
|
algo types.ChecksumAlgorithm
|
|
}{
|
|
{part.ChecksumCRC32, getString(checksum.CRC32), types.ChecksumAlgorithmCrc32},
|
|
{part.ChecksumCRC32C, getString(checksum.CRC32C), types.ChecksumAlgorithmCrc32c},
|
|
{part.ChecksumSHA1, getString(checksum.SHA1), types.ChecksumAlgorithmSha1},
|
|
{part.ChecksumSHA256, getString(checksum.SHA256), types.ChecksumAlgorithmSha256},
|
|
{part.ChecksumCRC64NVME, getString(checksum.CRC64NVME), types.ChecksumAlgorithmCrc64nvme},
|
|
{part.ChecksumSHA512, getString(checksum.SHA512), types.ChecksumAlgorithmSha512},
|
|
{part.ChecksumMD5, getString(checksum.MD5), types.ChecksumAlgorithmMd5},
|
|
{part.ChecksumXXHASH64, getString(checksum.XXHASH64), types.ChecksumAlgorithmXxhash64},
|
|
{part.ChecksumXXHASH3, getString(checksum.XXHASH3), types.ChecksumAlgorithmXxhash3},
|
|
{part.ChecksumXXHASH128, getString(checksum.XXHASH128), types.ChecksumAlgorithmXxhash128},
|
|
} {
|
|
if cs.checksum == nil {
|
|
continue
|
|
}
|
|
|
|
if !utils.IsValidChecksum(*cs.checksum, cs.algo) {
|
|
return s3err.GetInvalidArgumentErr(s3err.InvalidArgChecksumPart, *cs.checksum)
|
|
}
|
|
|
|
if *cs.checksum != cs.expectedChecksum {
|
|
if algo == cs.algo {
|
|
return s3err.GetInvalidPartErr(uploadId, *part.PartNumber, *part.ETag)
|
|
}
|
|
|
|
return s3err.APIError{
|
|
Code: "BadDigest",
|
|
Description: fmt.Sprintf("The %v you specified for part %v did not match what we received.", strings.ToLower(string(cs.algo)), *part.PartNumber),
|
|
HTTPStatusCode: http.StatusBadRequest,
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func numberOfChecksums(part types.CompletedPart) (int, string) {
|
|
counter := 0
|
|
builder := &strings.Builder{}
|
|
|
|
for _, ch := range []struct {
|
|
algo types.ChecksumAlgorithm
|
|
value *string
|
|
}{
|
|
{types.ChecksumAlgorithmCrc32, part.ChecksumCRC32},
|
|
{types.ChecksumAlgorithmCrc32c, part.ChecksumCRC32C},
|
|
{types.ChecksumAlgorithmCrc64nvme, part.ChecksumCRC64NVME},
|
|
{types.ChecksumAlgorithmSha1, part.ChecksumSHA1},
|
|
{types.ChecksumAlgorithmSha256, part.ChecksumSHA256},
|
|
} {
|
|
if getString(ch.value) != "" {
|
|
counter++
|
|
fmt.Fprintf(builder, "%s:%s;", string(ch.algo), getString(ch.value))
|
|
}
|
|
}
|
|
if getString(part.ChecksumSHA512) != "" {
|
|
counter++
|
|
}
|
|
if getString(part.ChecksumMD5) != "" {
|
|
counter++
|
|
}
|
|
if getString(part.ChecksumXXHASH64) != "" {
|
|
counter++
|
|
}
|
|
if getString(part.ChecksumXXHASH3) != "" {
|
|
counter++
|
|
}
|
|
if getString(part.ChecksumXXHASH128) != "" {
|
|
counter++
|
|
}
|
|
|
|
return counter, builder.String()
|
|
}
|
|
|
|
func (p *Posix) checkUploadIDExists(bucket, object, uploadID string) ([32]byte, error) {
|
|
sum := sha256.Sum256([]byte(object))
|
|
objdir := filepath.Join(p.BucketPath(bucket), MetaTmpMultipartDir, fmt.Sprintf("%x", sum))
|
|
|
|
_, err := os.Stat(filepath.Join(objdir, uploadID))
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return [32]byte{}, s3err.GetNoSuchUploadErr(uploadID)
|
|
}
|
|
if err != nil {
|
|
return [32]byte{}, fmt.Errorf("stat upload: %w", err)
|
|
}
|
|
return sum, nil
|
|
}
|
|
|
|
type metaProperties struct {
|
|
ContentType *string
|
|
ContentEncoding *string
|
|
ContentDisposition *string
|
|
ContentLanguage *string
|
|
CacheControl *string
|
|
Expires *string
|
|
WebsiteRedirectLocation *string
|
|
Metadata map[string]string
|
|
}
|
|
|
|
// loadObjectMetadata loads the given object metadata, if it fails to load
|
|
// it falls back to the old metadata key -> xattr key mapping mechanism
|
|
// and converts the old metadata storing schema with the new one
|
|
func (p *Posix) loadObjectMetadata(f *os.File, bucket, object string) map[string]string {
|
|
m, err := p.meta.RetrieveAttribute(f, bucket, object, metadataHdr)
|
|
if err != nil {
|
|
if errors.Is(err, meta.ErrNoSuchKey) {
|
|
// fallback to the deprecated mechanism
|
|
ents, err := p.meta.ListAttributes(bucket, object)
|
|
if err != nil || len(ents) == 0 {
|
|
return nil
|
|
}
|
|
result := map[string]string{}
|
|
|
|
legacyAttrs := make([]string, 0, len(ents))
|
|
for _, e := range ents {
|
|
if !isValidMeta(e) {
|
|
continue
|
|
}
|
|
b, err := p.meta.RetrieveAttribute(f, bucket, object, e)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if b == nil {
|
|
b = []byte{}
|
|
}
|
|
|
|
legacyAttrs = append(legacyAttrs, e)
|
|
result[strings.TrimPrefix(e, fmt.Sprintf("%v.", oldMetaHdr))] = string(b)
|
|
}
|
|
|
|
if len(result) != 0 {
|
|
err = p.storeObjectMetadata(f, bucket, object, result)
|
|
if err == nil {
|
|
// if it succeeded to store the metadata as a json
|
|
// object in user.metadata, cleanup the legacy X-Amz-Meta.
|
|
// attributes
|
|
for _, attr := range legacyAttrs {
|
|
_ = p.meta.DeleteAttribute(bucket, object, attr)
|
|
}
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var metadata map[string]string
|
|
_ = json.Unmarshal(m, &metadata)
|
|
return metadata
|
|
}
|
|
|
|
// loadObjectMetaProperties loads the given object meta properties
|
|
// including Content-Type, Cache-Control ... and the object Metadata
|
|
func (p *Posix) loadObjectMetaProperties(f *os.File, bucket, object string, fi *os.FileInfo) metaProperties {
|
|
var result metaProperties
|
|
|
|
b, err := p.meta.RetrieveAttribute(f, bucket, object, contentTypeHdr)
|
|
if err == nil {
|
|
result.ContentType = backend.GetPtrFromString(string(b))
|
|
}
|
|
|
|
if (result.ContentType == nil || *result.ContentType == "") && fi != nil {
|
|
if (*fi).IsDir() {
|
|
// this is the media type for directories in AWS and Nextcloud
|
|
result.ContentType = backend.GetPtrFromString("application/x-directory")
|
|
}
|
|
}
|
|
|
|
b, err = p.meta.RetrieveAttribute(f, bucket, object, contentEncHdr)
|
|
if err == nil {
|
|
result.ContentEncoding = backend.GetPtrFromString(string(b))
|
|
}
|
|
|
|
b, err = p.meta.RetrieveAttribute(f, bucket, object, contentDispHdr)
|
|
if err == nil {
|
|
result.ContentDisposition = backend.GetPtrFromString(string(b))
|
|
}
|
|
|
|
b, err = p.meta.RetrieveAttribute(f, bucket, object, contentLangHdr)
|
|
if err == nil {
|
|
result.ContentLanguage = backend.GetPtrFromString(string(b))
|
|
}
|
|
|
|
b, err = p.meta.RetrieveAttribute(f, bucket, object, cacheCtrlHdr)
|
|
if err == nil {
|
|
result.CacheControl = backend.GetPtrFromString(string(b))
|
|
}
|
|
|
|
b, err = p.meta.RetrieveAttribute(f, bucket, object, expiresHdr)
|
|
if err == nil {
|
|
result.Expires = backend.GetPtrFromString(string(b))
|
|
}
|
|
|
|
b, err = p.meta.RetrieveAttribute(f, bucket, object, websiteRedirectHdr)
|
|
if err == nil {
|
|
result.WebsiteRedirectLocation = backend.GetPtrFromString(string(b))
|
|
}
|
|
|
|
result.Metadata = p.loadObjectMetadata(f, bucket, object)
|
|
|
|
return result
|
|
}
|
|
|
|
// storeObjectMetadata parses the given metadata map[string]string
|
|
// to raw json to store in file/directory meta storage and checks/cleans up
|
|
// any legacy metadata persent in the object meta storage
|
|
func (p *Posix) storeObjectMetadata(f *os.File, bucket, object string, meta map[string]string) error {
|
|
if len(meta) == 0 {
|
|
return nil
|
|
}
|
|
|
|
parsed, err := json.Marshal(meta)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal metadata: %w", err)
|
|
}
|
|
|
|
err = p.meta.StoreAttribute(f, bucket, object, metadataHdr, parsed)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// cleanup any previously set legacy metadata
|
|
ents, err := p.meta.ListAttributes(bucket, object)
|
|
if err != nil || len(ents) == 0 {
|
|
return nil
|
|
}
|
|
|
|
for _, ent := range ents {
|
|
if !isValidMeta(ent) {
|
|
continue
|
|
}
|
|
|
|
_ = p.meta.DeleteAttribute(bucket, object, ent)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// storeObjectMetaProperties stores the object meta properties like:
|
|
// Content-Type, Content-Encoding, object Metadata ...
|
|
func (p *Posix) storeObjectMetaProperties(f *os.File, bucket, object string, m metaProperties) error {
|
|
if getString(m.ContentType) != "" {
|
|
err := p.meta.StoreAttribute(f, bucket, object, contentTypeHdr, []byte(*m.ContentType))
|
|
if err != nil {
|
|
return fmt.Errorf("set content-type: %w", err)
|
|
}
|
|
}
|
|
if getString(m.ContentEncoding) != "" {
|
|
err := p.meta.StoreAttribute(f, bucket, object, contentEncHdr, []byte(*m.ContentEncoding))
|
|
if err != nil {
|
|
return fmt.Errorf("set content-encoding: %w", err)
|
|
}
|
|
}
|
|
if getString(m.ContentDisposition) != "" {
|
|
err := p.meta.StoreAttribute(f, bucket, object, contentDispHdr, []byte(*m.ContentDisposition))
|
|
if err != nil {
|
|
return fmt.Errorf("set content-disposition: %w", err)
|
|
}
|
|
}
|
|
if getString(m.ContentLanguage) != "" {
|
|
err := p.meta.StoreAttribute(f, bucket, object, contentLangHdr, []byte(*m.ContentLanguage))
|
|
if err != nil {
|
|
return fmt.Errorf("set content-language: %w", err)
|
|
}
|
|
}
|
|
if getString(m.CacheControl) != "" {
|
|
err := p.meta.StoreAttribute(f, bucket, object, cacheCtrlHdr, []byte(*m.CacheControl))
|
|
if err != nil {
|
|
return fmt.Errorf("set cache-control: %w", err)
|
|
}
|
|
}
|
|
if getString(m.Expires) != "" {
|
|
err := p.meta.StoreAttribute(f, bucket, object, expiresHdr, []byte(*m.Expires))
|
|
if err != nil {
|
|
return fmt.Errorf("set expires: %w", err)
|
|
}
|
|
}
|
|
if getString(m.WebsiteRedirectLocation) != "" {
|
|
err := p.meta.StoreAttribute(f, bucket, object, websiteRedirectHdr, []byte(*m.WebsiteRedirectLocation))
|
|
if err != nil {
|
|
return fmt.Errorf("set website-redirect-location: %w", err)
|
|
}
|
|
}
|
|
if m.Metadata != nil {
|
|
err := p.storeObjectMetadata(f, bucket, object, m.Metadata)
|
|
if err != nil {
|
|
return fmt.Errorf("set metadata: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func isValidMeta(val string) bool {
|
|
return strings.HasPrefix(val, oldMetaHdr)
|
|
}
|
|
|
|
func (p *Posix) AbortMultipartUpload(ctx context.Context, mpu *s3.AbortMultipartUploadInput) error {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer release()
|
|
|
|
if mpu.Key == nil {
|
|
return s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
if mpu.UploadId == nil {
|
|
return s3err.GetNoSuchUploadErr("")
|
|
}
|
|
|
|
bucket := *mpu.Bucket
|
|
object := *mpu.Key
|
|
uploadID := *mpu.UploadId
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
|
|
err = p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
sum := sha256.Sum256([]byte(object))
|
|
objdir := filepath.Join(p.BucketPath(bucket), MetaTmpMultipartDir, fmt.Sprintf("%x", sum))
|
|
|
|
f, err := os.Stat(filepath.Join(objdir, uploadID))
|
|
if err != nil {
|
|
return s3err.GetNoSuchUploadErr(uploadID)
|
|
}
|
|
|
|
if mpu.IfMatchInitiatedTime != nil {
|
|
if mpu.IfMatchInitiatedTime.Unix() != f.ModTime().Unix() {
|
|
return s3err.GetPreconditionFailedErr(s3err.ConditionIfMatchInitiatedTime)
|
|
}
|
|
}
|
|
|
|
err = os.RemoveAll(filepath.Join(objdir, uploadID))
|
|
if err != nil {
|
|
return fmt.Errorf("remove multipart upload container: %w", err)
|
|
}
|
|
os.Remove(objdir)
|
|
|
|
// Clean up sidecar metadata for the aborted upload. With xattr this is
|
|
// a no-op; with sidecar the metadata directory would otherwise be orphaned.
|
|
uploadMetaPath := filepath.Join(MetaTmpMultipartDir, fmt.Sprintf("%x", sum), uploadID)
|
|
_ = p.meta.DeleteAttributes(bucket, uploadMetaPath)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Posix) ListMultipartUploads(ctx context.Context, mpu *s3.ListMultipartUploadsInput) (s3response.ListMultipartUploadsResult, error) {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return s3response.ListMultipartUploadsResult{}, err
|
|
}
|
|
defer release()
|
|
|
|
var lmu s3response.ListMultipartUploadsResult
|
|
|
|
bucket := *mpu.Bucket
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return lmu, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
|
|
var delimiter string
|
|
if mpu.Delimiter != nil {
|
|
delimiter = *mpu.Delimiter
|
|
}
|
|
var prefix string
|
|
if mpu.Prefix != nil {
|
|
prefix = *mpu.Prefix
|
|
}
|
|
var keyMarker string
|
|
if mpu.KeyMarker != nil {
|
|
keyMarker = *mpu.KeyMarker
|
|
}
|
|
var uploadIDMarker string
|
|
if mpu.UploadIdMarker != nil {
|
|
uploadIDMarker = *mpu.UploadIdMarker
|
|
}
|
|
maxUploads := int(*mpu.MaxUploads)
|
|
|
|
err = p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return lmu, err
|
|
}
|
|
|
|
// ignore readdir error and use the empty list returned
|
|
objs, _ := os.ReadDir(p.ObjectPath(bucket, MetaTmpMultipartDir))
|
|
|
|
var uploads []s3response.Upload
|
|
|
|
for _, obj := range objs {
|
|
if !obj.IsDir() {
|
|
continue
|
|
}
|
|
|
|
b, err := p.meta.RetrieveAttribute(nil, bucket, filepath.Join(MetaTmpMultipartDir, obj.Name()), onameAttr)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
objectName := string(b)
|
|
// filter by prefix
|
|
if prefix != "" && !strings.HasPrefix(objectName, prefix) {
|
|
continue
|
|
}
|
|
// filter by keyMarker
|
|
if keyMarker != "" && objectName <= keyMarker {
|
|
continue
|
|
}
|
|
|
|
upids, err := os.ReadDir(filepath.Join(p.BucketPath(bucket), MetaTmpMultipartDir, obj.Name()))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
for _, upid := range upids {
|
|
if !upid.IsDir() {
|
|
continue
|
|
}
|
|
// skip directories that are currently being completed
|
|
if strings.HasSuffix(upid.Name(), inProgressSuffix) {
|
|
continue
|
|
}
|
|
|
|
fi, err := upid.Info()
|
|
if err != nil {
|
|
return lmu, fmt.Errorf("stat %q: %w", upid.Name(), err)
|
|
}
|
|
|
|
uploadID := upid.Name()
|
|
|
|
checksum, err := p.retrieveChecksums(nil, bucket, filepath.Join(MetaTmpMultipartDir, obj.Name(), uploadID))
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return lmu, fmt.Errorf("get mp checksum: %w", err)
|
|
}
|
|
|
|
uploads = append(uploads, s3response.Upload{
|
|
Key: objectName,
|
|
UploadID: uploadID,
|
|
StorageClass: types.StorageClassStandard,
|
|
Initiated: fi.ModTime(),
|
|
ChecksumAlgorithm: checksum.Algorithm,
|
|
ChecksumType: checksum.Type,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Sort once: Key asc, Initiated asc, UploadID asc
|
|
// UploadID is a tie-breaker for uploads that share both key and
|
|
// initiated time, since directory mtime resolution isn't always
|
|
// fine-grained enough to distinguish uploads created back to back.
|
|
sort.Slice(uploads, func(i, j int) bool {
|
|
if uploads[i].Key != uploads[j].Key {
|
|
return uploads[i].Key < uploads[j].Key
|
|
}
|
|
if !uploads[i].Initiated.Equal(uploads[j].Initiated) {
|
|
return uploads[i].Initiated.Before(uploads[j].Initiated)
|
|
}
|
|
return uploads[i].UploadID < uploads[j].UploadID
|
|
})
|
|
|
|
result, err := backend.ListMultipartUploads(uploads, prefix, delimiter, keyMarker, uploadIDMarker, maxUploads)
|
|
if err != nil {
|
|
return lmu, err
|
|
}
|
|
|
|
return s3response.ListMultipartUploadsResult{
|
|
Bucket: bucket,
|
|
Delimiter: delimiter,
|
|
KeyMarker: keyMarker,
|
|
MaxUploads: maxUploads,
|
|
Prefix: prefix,
|
|
NextKeyMarker: result.NextKeyMarker,
|
|
NextUploadIDMarker: result.NextUploadIDMarker,
|
|
UploadIDMarker: uploadIDMarker,
|
|
IsTruncated: result.IsTruncated,
|
|
Uploads: result.Uploads,
|
|
CommonPrefixes: result.CommonPrefixes,
|
|
}, nil
|
|
}
|
|
|
|
func (p *Posix) ListParts(ctx context.Context, input *s3.ListPartsInput) (s3response.ListPartsResult, error) {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return s3response.ListPartsResult{}, err
|
|
}
|
|
defer release()
|
|
|
|
var lpr s3response.ListPartsResult
|
|
|
|
if input.Key == nil {
|
|
return lpr, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
if input.UploadId == nil {
|
|
return lpr, s3err.GetNoSuchUploadErr("")
|
|
}
|
|
|
|
bucket := *input.Bucket
|
|
object := *input.Key
|
|
uploadID := *input.UploadId
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return lpr, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
stringMarker := ""
|
|
if input.PartNumberMarker != nil {
|
|
stringMarker = *input.PartNumberMarker
|
|
}
|
|
|
|
maxParts := int(*input.MaxParts)
|
|
|
|
var partNumberMarker int
|
|
if stringMarker != "" {
|
|
var err error
|
|
partNumberMarker, err = strconv.Atoi(stringMarker)
|
|
if err != nil {
|
|
return lpr, s3err.GetInvalidArgMaxLimiter("part-number-marker", *input.PartNumberMarker)
|
|
}
|
|
}
|
|
|
|
err = p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return lpr, err
|
|
}
|
|
|
|
sum, err := p.checkUploadIDExists(bucket, object, uploadID)
|
|
if err != nil {
|
|
return lpr, err
|
|
}
|
|
|
|
objdir := filepath.Join(MetaTmpMultipartDir, fmt.Sprintf("%x", sum))
|
|
tmpdir := p.ObjectPath(bucket, objdir)
|
|
|
|
ents, err := os.ReadDir(filepath.Join(tmpdir, uploadID))
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return lpr, s3err.GetNoSuchUploadErr(uploadID)
|
|
}
|
|
if err != nil {
|
|
return lpr, fmt.Errorf("readdir upload: %w", err)
|
|
}
|
|
|
|
checksum, err := p.retrieveChecksums(nil, bucket, filepath.Join(objdir, uploadID))
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return lpr, fmt.Errorf("get mp checksum: %w", err)
|
|
}
|
|
if checksum.Algorithm == "" {
|
|
checksum.Algorithm = types.ChecksumAlgorithm("null")
|
|
}
|
|
if checksum.Type == "" {
|
|
checksum.Type = types.ChecksumType("null")
|
|
}
|
|
|
|
parts := make([]s3response.Part, 0, len(ents))
|
|
for i, e := range ents {
|
|
if i%128 == 0 {
|
|
select {
|
|
case <-ctx.Done():
|
|
return s3response.ListPartsResult{}, ctx.Err()
|
|
default:
|
|
}
|
|
}
|
|
pn, err := strconv.Atoi(e.Name())
|
|
if err != nil {
|
|
// file is not a valid part file
|
|
continue
|
|
}
|
|
if pn <= partNumberMarker {
|
|
continue
|
|
}
|
|
|
|
partPath := filepath.Join(objdir, uploadID, e.Name())
|
|
b, err := p.meta.RetrieveAttribute(nil, bucket, partPath, etagkey)
|
|
etag := string(b)
|
|
if err != nil {
|
|
etag = ""
|
|
}
|
|
|
|
checksum, err := p.retrieveChecksums(nil, bucket, partPath)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
continue
|
|
}
|
|
|
|
fi, err := os.Lstat(p.ObjectPath(bucket, partPath))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
parts = append(parts, s3response.Part{
|
|
PartNumber: pn,
|
|
ETag: etag,
|
|
LastModified: fi.ModTime(),
|
|
Size: fi.Size(),
|
|
ChecksumCRC32: checksum.CRC32,
|
|
ChecksumCRC32C: checksum.CRC32C,
|
|
ChecksumSHA1: checksum.SHA1,
|
|
ChecksumSHA256: checksum.SHA256,
|
|
ChecksumCRC64NVME: checksum.CRC64NVME,
|
|
ChecksumSHA512: checksum.SHA512,
|
|
ChecksumMD5: checksum.MD5,
|
|
ChecksumXXHASH64: checksum.XXHASH64,
|
|
ChecksumXXHASH3: checksum.XXHASH3,
|
|
ChecksumXXHASH128: checksum.XXHASH128,
|
|
})
|
|
}
|
|
|
|
sort.Slice(parts,
|
|
func(i int, j int) bool { return parts[i].PartNumber < parts[j].PartNumber })
|
|
|
|
oldLen := len(parts)
|
|
if maxParts > 0 && len(parts) > maxParts {
|
|
parts = parts[:maxParts]
|
|
}
|
|
newLen := len(parts)
|
|
|
|
nextpart := 0
|
|
if len(parts) != 0 {
|
|
nextpart = parts[len(parts)-1].PartNumber
|
|
}
|
|
|
|
return s3response.ListPartsResult{
|
|
Bucket: bucket,
|
|
IsTruncated: oldLen != newLen,
|
|
Key: object,
|
|
MaxParts: maxParts,
|
|
NextPartNumberMarker: nextpart,
|
|
PartNumberMarker: partNumberMarker,
|
|
Parts: parts,
|
|
UploadID: uploadID,
|
|
StorageClass: types.StorageClassStandard,
|
|
ChecksumAlgorithm: checksum.Algorithm,
|
|
ChecksumType: checksum.Type,
|
|
}, nil
|
|
}
|
|
|
|
type hashConfig struct {
|
|
value *string
|
|
hashType utils.HashType
|
|
}
|
|
|
|
func (p *Posix) UploadPart(ctx context.Context, input *s3.UploadPartInput) (*s3.UploadPartOutput, error) {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer release()
|
|
|
|
return p.UploadPartWithPostFunc(ctx, input, func(*os.File) error { return nil })
|
|
}
|
|
|
|
func (p *Posix) UploadPartWithPostFunc(ctx context.Context, input *s3.UploadPartInput, postprocess func(f *os.File) error) (*s3.UploadPartOutput, error) {
|
|
acct, ok := ctx.Value("account").(auth.Account)
|
|
if !ok {
|
|
acct = auth.Account{}
|
|
}
|
|
|
|
if input.Key == nil {
|
|
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
|
|
bucket := *input.Bucket
|
|
object := *input.Key
|
|
uploadID := *input.UploadId
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return nil, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
part := input.PartNumber
|
|
length := int64(0)
|
|
if input.ContentLength != nil {
|
|
length = *input.ContentLength
|
|
}
|
|
r := input.Body
|
|
|
|
err := p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sum := sha256.Sum256([]byte(object))
|
|
objdir := filepath.Join(MetaTmpMultipartDir, fmt.Sprintf("%x", sum))
|
|
mpPath := filepath.Join(objdir, uploadID)
|
|
|
|
_, err = os.Stat(p.ObjectPath(bucket, mpPath))
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return nil, s3err.GetNoSuchUploadErr(uploadID)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("stat uploadid: %w", err)
|
|
}
|
|
|
|
partPath := filepath.Join(mpPath, fmt.Sprintf("%v", *part))
|
|
|
|
f, err := p.openTmpFile(p.ObjectPath(bucket, objdir),
|
|
bucket, partPath, length, acct, doFalloc, p.forceNoTmpFile, odirectAllowed)
|
|
if err != nil {
|
|
if errors.Is(err, syscall.EDQUOT) {
|
|
drainBody(r)
|
|
return nil, s3err.GetAPIError(s3err.ErrQuotaExceeded)
|
|
}
|
|
if errors.Is(err, syscall.ENOSPC) {
|
|
drainBody(r)
|
|
return nil, s3err.GetAPIError(s3err.ErrNoSpaceLeftOnDevice)
|
|
}
|
|
return nil, fmt.Errorf("open temp file: %w", err)
|
|
}
|
|
defer f.cleanup()
|
|
|
|
// When dataIntegrityEtag is enabled, MD5 is never used for the part ETag,
|
|
// so skip both md5.New() and the per-byte TeeReader overhead.
|
|
var md5hash hash.Hash
|
|
var tr io.Reader
|
|
if p.dataIntegrityEtag {
|
|
tr = r
|
|
} else {
|
|
md5hash = md5.New()
|
|
tr = io.TeeReader(r, md5hash)
|
|
}
|
|
|
|
chRdr, chunkUpload := input.Body.(middlewares.ChecksumReader)
|
|
isTrailingChecksum := chunkUpload && chRdr.Algorithm() != ""
|
|
|
|
// user input checksum algorithm: either with chunk uploads or with request headers
|
|
var inputChAlgo utils.HashType
|
|
// user input checksum value specified with request headers
|
|
var inputSum string
|
|
|
|
if !isTrailingChecksum {
|
|
hashConfigs := []hashConfig{
|
|
{input.ChecksumCRC32, utils.HashTypeCRC32},
|
|
{input.ChecksumCRC32C, utils.HashTypeCRC32C},
|
|
{input.ChecksumSHA1, utils.HashTypeSha1},
|
|
{input.ChecksumSHA256, utils.HashTypeSha256},
|
|
{input.ChecksumCRC64NVME, utils.HashTypeCRC64NVME},
|
|
{input.ChecksumSHA512, utils.HashTypeSha512},
|
|
{input.ChecksumMD5, utils.HashTypeMd5},
|
|
{input.ChecksumXXHASH64, utils.HashTypeXXHASH64},
|
|
{input.ChecksumXXHASH3, utils.HashTypeXXHASH3},
|
|
{input.ChecksumXXHASH128, utils.HashTypeXXHASH128},
|
|
}
|
|
|
|
for _, config := range hashConfigs {
|
|
if config.value != nil {
|
|
inputChAlgo = config.hashType
|
|
inputSum = *config.value
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
inputChAlgo = utils.HashType(chRdr.Algorithm())
|
|
}
|
|
|
|
exposeChecksum := inputChAlgo != ""
|
|
|
|
checksums, err := p.retrieveChecksums(nil, bucket, mpPath)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return nil, fmt.Errorf("retrieve mp checksum: %w", err)
|
|
}
|
|
|
|
// If checksum isn't provided for the part,
|
|
// but it has been provided on mp initialization
|
|
// and checksum type is 'COMPOSITE', return mismatch error
|
|
if inputChAlgo == "" && checksums.Type == types.ChecksumTypeComposite {
|
|
return nil, s3err.GetChecksumTypeMismatchErr(checksums.Algorithm, "null")
|
|
}
|
|
|
|
// Check if the provided checksum algorithm match
|
|
// the one specified on mp initialization
|
|
if inputChAlgo != "" && checksums.Type != "" {
|
|
algo := types.ChecksumAlgorithm(strings.ToUpper(string(inputChAlgo)))
|
|
if checksums.Algorithm != algo {
|
|
return nil, s3err.GetChecksumTypeMismatchErr(checksums.Algorithm, algo)
|
|
}
|
|
}
|
|
|
|
if inputChAlgo == "" {
|
|
// default to crc64nvme
|
|
inputChAlgo = utils.HashTypeCRC64NVME
|
|
}
|
|
|
|
// hashreader is responsible to calculate and validate
|
|
// user input checksums
|
|
var hashRdr *utils.HashReader
|
|
// crc64nvmeRdr is used to calculate the object crc64nvme
|
|
// only for internal usage
|
|
var crc64nvmeRdr *utils.HashReader
|
|
|
|
if checksums.Type == "" {
|
|
if inputChAlgo != utils.HashTypeCRC64NVME {
|
|
// if the input checksum algorithm isn't crc64nvme, create a
|
|
// crc64nvme reader no matter if checksum is
|
|
// received from chunk reader or request headers.
|
|
crc64nvmeRdr, err = utils.NewHashReader(tr, "", utils.HashTypeCRC64NVME)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("initialize crc64nvme hash reader: %w", err)
|
|
}
|
|
tr = crc64nvmeRdr
|
|
}
|
|
|
|
if !isTrailingChecksum {
|
|
// create a new hash reader for the user input checksum for calculation
|
|
// if the checksum doesn't come from chunk readers
|
|
hashRdr, err = utils.NewHashReader(tr, inputSum, inputChAlgo)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("initialize hash reader: %w", err)
|
|
}
|
|
|
|
tr = hashRdr
|
|
}
|
|
} else {
|
|
// if no checksum algorithm or precalculated checksum is
|
|
// provided, but one has been on multipart upload initialization,
|
|
// anyways calculate and store the uploaded part checksum
|
|
if !isTrailingChecksum {
|
|
chAlgo := utils.HashType(strings.ToLower(string(checksums.Algorithm)))
|
|
hashRdr, err = utils.NewHashReader(tr, inputSum, chAlgo)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("initialize hash reader: %w", err)
|
|
}
|
|
|
|
tr = hashRdr
|
|
}
|
|
// When dataIntegrityEtag is enabled and the mp algo isn't CRC64NVME,
|
|
// wrap the reader chain with an internal CRC64NVME reader so the part
|
|
// ETag can be CRC64NVME-based (trailing or non-trailing).
|
|
if p.dataIntegrityEtag && checksums.Algorithm != types.ChecksumAlgorithmCrc64nvme {
|
|
crc64nvmeRdr, err = utils.NewHashReader(tr, "", utils.HashTypeCRC64NVME)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("initialize internal crc64nvme reader: %w", err)
|
|
}
|
|
tr = crc64nvmeRdr
|
|
}
|
|
}
|
|
|
|
buf := p.getIOBuffer()
|
|
defer p.putIOBuffer(buf)
|
|
|
|
_, err = io.CopyBuffer(f, tr, buf)
|
|
if err != nil {
|
|
if errors.Is(err, syscall.EDQUOT) {
|
|
drainBody(tr)
|
|
return nil, s3err.GetAPIError(s3err.ErrQuotaExceeded)
|
|
}
|
|
if errors.Is(err, syscall.ENOSPC) {
|
|
drainBody(tr)
|
|
return nil, s3err.GetAPIError(s3err.ErrNoSpaceLeftOnDevice)
|
|
}
|
|
// Return the error itself, if it implements the s3err.S3Error interface
|
|
if _, ok := err.(s3err.S3Error); ok {
|
|
return nil, err
|
|
}
|
|
return nil, fmt.Errorf("write part data: %w", err)
|
|
}
|
|
|
|
// Generate the part ETag.
|
|
// When dataIntegrityEtag is enabled, use CRC64NVME instead of MD5.
|
|
// CRC64NVME is available from: hashRdr/chRdr when the mp or input algo
|
|
// is already CRC64NVME, or from crc64nvmeRdr which is always set up in
|
|
// the non-CRC64NVME paths (checksums.Type=="" branch) or was just added
|
|
// above in the storeChecksum branch.
|
|
var etag string
|
|
if p.dataIntegrityEtag {
|
|
var crc64Sum string
|
|
isCrc64 := checksums.Algorithm == types.ChecksumAlgorithmCrc64nvme ||
|
|
(checksums.Type == "" && inputChAlgo == utils.HashTypeCRC64NVME)
|
|
if isCrc64 {
|
|
if isTrailingChecksum {
|
|
crc64Sum = chRdr.Checksum()
|
|
} else {
|
|
crc64Sum = hashRdr.Sum()
|
|
}
|
|
} else {
|
|
crc64Sum = crc64nvmeRdr.Sum()
|
|
}
|
|
etag = fmt.Sprintf("\"CRC64NVME-%s\"", crc64Sum)
|
|
} else {
|
|
etag = backend.GenerateEtag(md5hash)
|
|
}
|
|
err = p.meta.StoreAttribute(f.File(), bucket, partPath, etagkey, []byte(etag))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("set etag attr: %w", err)
|
|
}
|
|
|
|
res := &s3.UploadPartOutput{
|
|
ETag: &etag,
|
|
}
|
|
|
|
// if a checksum algorithm has been provided on mp initiation
|
|
// the checksums should be stored, otherwise only returned
|
|
// in the response without storing
|
|
storeChecksum := checksums.Type != ""
|
|
|
|
if storeChecksum {
|
|
checksum := s3response.Checksum{
|
|
Algorithm: checksums.Algorithm,
|
|
}
|
|
|
|
var sum string
|
|
if isTrailingChecksum {
|
|
sum = chRdr.Checksum()
|
|
}
|
|
if hashRdr != nil {
|
|
sum = hashRdr.Sum()
|
|
}
|
|
|
|
checksum.SetSum(checksums.Algorithm, &sum)
|
|
setUploadPartChecksum(res, checksums.Algorithm, &sum)
|
|
|
|
err := p.storeChecksums(f.File(), bucket, partPath, checksum)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("store checksum: %w", err)
|
|
}
|
|
} else {
|
|
var internalCrc64NvmeSum string
|
|
if inputChAlgo == utils.HashTypeCRC64NVME {
|
|
if isTrailingChecksum {
|
|
internalCrc64NvmeSum = chRdr.Checksum()
|
|
} else {
|
|
internalCrc64NvmeSum = hashRdr.Sum()
|
|
}
|
|
} else {
|
|
internalCrc64NvmeSum = crc64nvmeRdr.Sum()
|
|
}
|
|
|
|
err := p.meta.StoreAttribute(f.File(), bucket, partPath, partCrc64nvme, []byte(internalCrc64NvmeSum))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("store part internal crc64nvme: %w", err)
|
|
}
|
|
|
|
if exposeChecksum {
|
|
var sumToReturn string
|
|
if isTrailingChecksum {
|
|
sumToReturn = chRdr.Checksum()
|
|
} else {
|
|
sumToReturn = hashRdr.Sum()
|
|
}
|
|
|
|
switch inputChAlgo {
|
|
case utils.HashTypeCRC32:
|
|
res.ChecksumCRC32 = &sumToReturn
|
|
case utils.HashTypeCRC32C:
|
|
res.ChecksumCRC32C = &sumToReturn
|
|
case utils.HashTypeSha1:
|
|
res.ChecksumSHA1 = &sumToReturn
|
|
case utils.HashTypeSha256:
|
|
res.ChecksumSHA256 = &sumToReturn
|
|
case utils.HashTypeCRC64NVME:
|
|
res.ChecksumCRC64NVME = &sumToReturn
|
|
case utils.HashTypeSha512:
|
|
res.ChecksumSHA512 = &sumToReturn
|
|
case utils.HashTypeMd5:
|
|
res.ChecksumMD5 = &sumToReturn
|
|
case utils.HashTypeXXHASH64:
|
|
res.ChecksumXXHASH64 = &sumToReturn
|
|
case utils.HashTypeXXHASH3:
|
|
res.ChecksumXXHASH3 = &sumToReturn
|
|
case utils.HashTypeXXHASH128:
|
|
res.ChecksumXXHASH128 = &sumToReturn
|
|
}
|
|
}
|
|
}
|
|
|
|
err = postprocess(f.File())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("upload part post process failed: %w", err)
|
|
}
|
|
|
|
err = f.link()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("link object in namespace: %w", err)
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func (p *Posix) UploadPartCopy(ctx context.Context, upi *s3.UploadPartCopyInput) (s3response.CopyPartResult, error) {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return s3response.CopyPartResult{}, err
|
|
}
|
|
defer release()
|
|
|
|
acct, ok := ctx.Value("account").(auth.Account)
|
|
if !ok {
|
|
acct = auth.Account{}
|
|
}
|
|
|
|
if upi.Key == nil {
|
|
return s3response.CopyPartResult{}, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
|
|
if !p.isBucketValid(*upi.Bucket) {
|
|
return s3response.CopyPartResult{}, s3err.GetBucketErr(s3err.ErrInvalidBucketName, *upi.Bucket)
|
|
}
|
|
|
|
err = p.doesBucketExist(*upi.Bucket)
|
|
if err != nil {
|
|
return s3response.CopyPartResult{}, err
|
|
}
|
|
|
|
sum := sha256.Sum256([]byte(*upi.Key))
|
|
objdir := filepath.Join(MetaTmpMultipartDir, fmt.Sprintf("%x", sum))
|
|
|
|
_, err = os.Stat(filepath.Join(p.BucketPath(*upi.Bucket), objdir, *upi.UploadId))
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return s3response.CopyPartResult{}, s3err.GetNoSuchUploadErr(*upi.UploadId)
|
|
}
|
|
if isErrNameTooLong(err) {
|
|
return s3response.CopyPartResult{}, s3err.GetKeyTooLongErr(int64(len(*upi.Key)), 1024)
|
|
}
|
|
if err != nil {
|
|
return s3response.CopyPartResult{}, fmt.Errorf("stat uploadid: %w", err)
|
|
}
|
|
|
|
partPath := filepath.Join(objdir, *upi.UploadId, fmt.Sprintf("%v", *upi.PartNumber))
|
|
|
|
srcBucket, srcObject, srcVersionId, err := backend.ParseCopySource(*upi.CopySource)
|
|
if err != nil {
|
|
return s3response.CopyPartResult{}, err
|
|
}
|
|
if err := p.validateVersionId(srcVersionId); err != nil {
|
|
return s3response.CopyPartResult{}, err
|
|
}
|
|
if !p.isBucketValid(srcBucket) {
|
|
return s3response.CopyPartResult{}, s3err.GetBucketErr(s3err.ErrInvalidBucketName, srcBucket)
|
|
}
|
|
|
|
err = p.doesBucketExist(srcBucket)
|
|
if err != nil {
|
|
return s3response.CopyPartResult{}, err
|
|
}
|
|
|
|
if upi.ExpectedSourceBucketOwner != nil && *upi.ExpectedSourceBucketOwner != "" {
|
|
aclData, err := p.meta.RetrieveAttribute(nil, srcBucket, "", aclkey)
|
|
if err != nil {
|
|
return s3response.CopyPartResult{}, fmt.Errorf("get src bucket acl: %w", err)
|
|
}
|
|
srcAcl, err := auth.ParseACL(aclData)
|
|
if err != nil {
|
|
return s3response.CopyPartResult{}, err
|
|
}
|
|
if srcAcl.Owner != *upi.ExpectedSourceBucketOwner {
|
|
return s3response.CopyPartResult{}, s3err.GetAPIError(s3err.ErrAccessDenied)
|
|
}
|
|
}
|
|
|
|
vStatus, err := p.getBucketVersioningStatus(ctx, srcBucket)
|
|
if err != nil {
|
|
return s3response.CopyPartResult{}, err
|
|
}
|
|
vEnabled := p.isBucketVersioningEnabled(vStatus)
|
|
|
|
if srcVersionId != "" {
|
|
if !p.versioningEnabled() || !vEnabled {
|
|
return s3response.CopyPartResult{}, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, srcVersionId)
|
|
}
|
|
vId, err := p.meta.RetrieveAttribute(nil, srcBucket, srcObject, versionIdKey)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return s3response.CopyPartResult{}, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return s3response.CopyPartResult{}, fmt.Errorf("get src object version id: %w", err)
|
|
}
|
|
|
|
if string(vId) != srcVersionId {
|
|
srcBucket = filepath.Join(p.versioningDir, srcBucket)
|
|
srcObject = filepath.Join(genObjVersionKey(srcObject), srcVersionId)
|
|
}
|
|
}
|
|
|
|
objPath := p.ObjectPath(srcBucket, srcObject)
|
|
fi, err := os.Stat(objPath)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
if p.versioningEnabled() && vEnabled {
|
|
return s3response.CopyPartResult{}, s3err.GetNoSuchVersionErr(srcObject, srcVersionId)
|
|
}
|
|
return s3response.CopyPartResult{}, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
if isErrNameTooLong(err) {
|
|
return s3response.CopyPartResult{}, s3err.GetKeyTooLongErr(int64(len(srcObject)), 1024)
|
|
}
|
|
if err != nil {
|
|
return s3response.CopyPartResult{}, fmt.Errorf("stat object: %w", err)
|
|
}
|
|
if strings.HasSuffix(srcObject, "/") != fi.IsDir() {
|
|
return s3response.CopyPartResult{}, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
if err := p.checkCopySourceDeleteMarker(srcBucket, srcObject, srcVersionId); err != nil {
|
|
return s3response.CopyPartResult{}, err
|
|
}
|
|
// a directory object holds no data
|
|
srcSize := fi.Size()
|
|
if fi.IsDir() {
|
|
isObj, err := p.isDirObject(srcBucket, srcObject)
|
|
if err != nil {
|
|
return s3response.CopyPartResult{}, err
|
|
}
|
|
if !isObj {
|
|
return s3response.CopyPartResult{}, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
srcSize = 0
|
|
}
|
|
|
|
startOffset, length, err := backend.ParseCopySourceRange(srcSize, *upi.CopySourceRange)
|
|
if err != nil {
|
|
return s3response.CopyPartResult{}, err
|
|
}
|
|
if length > p.copyObjectThreshold {
|
|
return s3response.CopyPartResult{}, s3err.GetCopySourceObjectTooLargeErr(p.copyObjectThreshold)
|
|
}
|
|
|
|
srcf, err := os.Open(objPath)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return s3response.CopyPartResult{}, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
if err != nil {
|
|
return s3response.CopyPartResult{}, fmt.Errorf("open object: %w", err)
|
|
}
|
|
defer srcf.Close()
|
|
|
|
// evaluate preconditions
|
|
b, err := p.meta.RetrieveAttribute(srcf, srcBucket, srcObject, etagkey)
|
|
srcEtag := string(b)
|
|
if err != nil || srcEtag == "" {
|
|
srcEtag = p.defaultEtag
|
|
}
|
|
|
|
err = backend.EvaluatePreconditions(srcEtag, fi.ModTime(), backend.PreConditions{
|
|
IfMatch: upi.CopySourceIfMatch,
|
|
IfNoneMatch: upi.CopySourceIfNoneMatch,
|
|
IfModSince: upi.CopySourceIfModifiedSince,
|
|
IfUnmodeSince: upi.CopySourceIfUnmodifiedSince,
|
|
})
|
|
if err != nil {
|
|
return s3response.CopyPartResult{}, err
|
|
}
|
|
|
|
f, err := p.openTmpFile(p.ObjectPath(*upi.Bucket, objdir),
|
|
*upi.Bucket, partPath, length, acct, doFalloc, p.forceNoTmpFile, odirectNotAllowed)
|
|
if err != nil {
|
|
if errors.Is(err, syscall.EDQUOT) {
|
|
return s3response.CopyPartResult{}, s3err.GetAPIError(s3err.ErrQuotaExceeded)
|
|
}
|
|
if errors.Is(err, syscall.ENOSPC) {
|
|
return s3response.CopyPartResult{}, s3err.GetAPIError(s3err.ErrNoSpaceLeftOnDevice)
|
|
}
|
|
return s3response.CopyPartResult{}, fmt.Errorf("open temp file: %w", err)
|
|
}
|
|
defer f.cleanup()
|
|
|
|
rdr := io.NewSectionReader(srcf, startOffset, length)
|
|
var md5hash hash.Hash
|
|
var tr io.Reader = rdr
|
|
if !p.dataIntegrityEtag {
|
|
md5hash = md5.New()
|
|
tr = io.TeeReader(rdr, md5hash)
|
|
}
|
|
|
|
mpChecksums, err := p.retrieveChecksums(nil, *upi.Bucket, filepath.Join(objdir, *upi.UploadId))
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return s3response.CopyPartResult{}, fmt.Errorf("retrieve mp checksums: %w", err)
|
|
}
|
|
|
|
checksums, err := p.retrieveChecksums(nil, srcBucket, srcObject)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return s3response.CopyPartResult{}, fmt.Errorf("retrieve object part checksums: %w", err)
|
|
}
|
|
|
|
// TODO: Should the checksum be recalculated or just copied ?
|
|
var hashRdr *utils.HashReader
|
|
var crc64nvmeRdr *utils.HashReader
|
|
if mpChecksums.Algorithm != "" {
|
|
if checksums.Algorithm == "" || mpChecksums.Algorithm != checksums.Algorithm {
|
|
hashRdr, err = utils.NewHashReader(tr, "", utils.HashType(strings.ToLower(string(mpChecksums.Algorithm))))
|
|
if err != nil {
|
|
return s3response.CopyPartResult{}, fmt.Errorf("initialize hash reader: %w", err)
|
|
}
|
|
|
|
tr = hashRdr
|
|
}
|
|
} else {
|
|
// if no checksum has been specified on multipart upload initiation
|
|
// create an internal crc64nvme reader to calculate and stored the internal crc64nvme
|
|
crc64nvmeRdr, err = utils.NewHashReader(tr, "", utils.HashTypeCRC64NVME)
|
|
if err != nil {
|
|
return s3response.CopyPartResult{}, fmt.Errorf("initialize internal crc64nvme reader: %w", err)
|
|
}
|
|
tr = crc64nvmeRdr
|
|
}
|
|
|
|
if p.dataIntegrityEtag && crc64nvmeRdr == nil {
|
|
// still need a crc64nvme-derived ETag even though a different checksum is already being computed
|
|
crc64nvmeRdr, err = utils.NewHashReader(tr, "", utils.HashTypeCRC64NVME)
|
|
if err != nil {
|
|
return s3response.CopyPartResult{}, fmt.Errorf("initialize etag crc64nvme reader: %w", err)
|
|
}
|
|
tr = crc64nvmeRdr
|
|
}
|
|
|
|
_, err = io.Copy(f, tr)
|
|
if err != nil {
|
|
if errors.Is(err, syscall.EDQUOT) {
|
|
return s3response.CopyPartResult{}, s3err.GetAPIError(s3err.ErrQuotaExceeded)
|
|
}
|
|
if errors.Is(err, syscall.ENOSPC) {
|
|
return s3response.CopyPartResult{}, s3err.GetAPIError(s3err.ErrNoSpaceLeftOnDevice)
|
|
}
|
|
return s3response.CopyPartResult{}, fmt.Errorf("copy part data: %w", err)
|
|
}
|
|
|
|
if checksums.Algorithm != "" {
|
|
if mpChecksums.Algorithm == "" {
|
|
checksums = s3response.Checksum{}
|
|
} else {
|
|
if hashRdr == nil {
|
|
err := p.storeChecksums(f.File(), *upi.Bucket, partPath, checksums)
|
|
if err != nil {
|
|
return s3response.CopyPartResult{}, fmt.Errorf("store part checksum: %w", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if hashRdr != nil {
|
|
algo := types.ChecksumAlgorithm(strings.ToUpper(string(hashRdr.Type())))
|
|
checksums = s3response.Checksum{
|
|
Algorithm: algo,
|
|
}
|
|
|
|
sum := hashRdr.Sum()
|
|
checksums.SetSum(algo, &sum)
|
|
|
|
err := p.storeChecksums(f.File(), *upi.Bucket, partPath, checksums)
|
|
if err != nil {
|
|
return s3response.CopyPartResult{}, fmt.Errorf("store part checksum: %w", err)
|
|
}
|
|
}
|
|
|
|
if crc64nvmeRdr != nil {
|
|
// store the internal crc64nvme
|
|
internalCrc64NvmeSum := crc64nvmeRdr.Sum()
|
|
err := p.meta.StoreAttribute(f.File(), *upi.Bucket, partPath, partCrc64nvme, []byte(internalCrc64NvmeSum))
|
|
if err != nil {
|
|
return s3response.CopyPartResult{}, fmt.Errorf("store part internal crc64nvme: %w", err)
|
|
}
|
|
}
|
|
|
|
var etag string
|
|
if p.dataIntegrityEtag {
|
|
etag = fmt.Sprintf("\"CRC64NVME-%s\"", crc64nvmeRdr.Sum())
|
|
} else {
|
|
etag = backend.GenerateEtag(md5hash)
|
|
}
|
|
err = p.meta.StoreAttribute(f.File(), *upi.Bucket, partPath, etagkey, []byte(etag))
|
|
if err != nil {
|
|
return s3response.CopyPartResult{}, fmt.Errorf("set etag attr: %w", err)
|
|
}
|
|
|
|
err = f.link()
|
|
if err != nil {
|
|
return s3response.CopyPartResult{}, fmt.Errorf("link object in namespace: %w", err)
|
|
}
|
|
|
|
fi, err = os.Stat(p.ObjectPath(*upi.Bucket, partPath))
|
|
if err != nil {
|
|
return s3response.CopyPartResult{}, fmt.Errorf("stat part path: %w", err)
|
|
}
|
|
|
|
return s3response.CopyPartResult{
|
|
ETag: &etag,
|
|
LastModified: fi.ModTime(),
|
|
CopySourceVersionId: srcVersionId,
|
|
ChecksumCRC32: checksums.CRC32,
|
|
ChecksumCRC32C: checksums.CRC32C,
|
|
ChecksumSHA1: checksums.SHA1,
|
|
ChecksumSHA256: checksums.SHA256,
|
|
ChecksumCRC64NVME: checksums.CRC64NVME,
|
|
ChecksumSHA512: checksums.SHA512,
|
|
ChecksumMD5: checksums.MD5,
|
|
ChecksumXXHASH64: checksums.XXHASH64,
|
|
ChecksumXXHASH3: checksums.XXHASH3,
|
|
ChecksumXXHASH128: checksums.XXHASH128,
|
|
}, nil
|
|
}
|
|
|
|
// getEmptyChecksumValue returns the base64-encoded checksum
|
|
// for an empty payload for the given algorithm defaulting to crc64nvme
|
|
func getEmptyChecksumValue(algo types.ChecksumAlgorithm) string {
|
|
switch algo {
|
|
case types.ChecksumAlgorithmCrc32:
|
|
return "AAAAAA=="
|
|
case types.ChecksumAlgorithmCrc32c:
|
|
return "AAAAAA=="
|
|
case types.ChecksumAlgorithmSha1:
|
|
return "2jmj7l5rSw0yVb/vlWAYkK/YBwk="
|
|
case types.ChecksumAlgorithmSha256:
|
|
return "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="
|
|
case types.ChecksumAlgorithmSha512:
|
|
return "z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg=="
|
|
case types.ChecksumAlgorithmMd5:
|
|
return "1B2M2Y8AsgTpgAmY7PhCfg=="
|
|
case types.ChecksumAlgorithmXxhash64:
|
|
return "70bbN1HY6Zk="
|
|
case types.ChecksumAlgorithmXxhash3:
|
|
return "LQaABTjTlMI="
|
|
case types.ChecksumAlgorithmXxhash128:
|
|
return "maoG0wFHmNhgAcMkRo1Jfw=="
|
|
default:
|
|
// default to crc64nvme
|
|
return "AAAAAAAAAAA="
|
|
}
|
|
}
|
|
|
|
// checkPutPreconditions evaluates the conditional write headers against the
|
|
// object's current etag. Callers that publish an object must repeat this
|
|
// check while holding the object publish lock (lockObjectPublish) so that
|
|
// evaluation and publication are atomic per bucket/key.
|
|
func (p *Posix) checkPutPreconditions(bucket, object string, ifMatch, ifNoneMatch *string) error {
|
|
if ifMatch == nil && ifNoneMatch == nil {
|
|
return nil
|
|
}
|
|
if p.objectLockMode == ObjectLockModeNone {
|
|
return s3err.GetAPIError(s3err.ErrNotImplemented)
|
|
}
|
|
|
|
// the etag at the object path may be the one of the key with or
|
|
// without the trailing slash
|
|
_, err := p.statLiveObject(bucket, object)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return backend.EvaluateObjectPutPreconditions("", ifMatch, ifNoneMatch, false)
|
|
}
|
|
|
|
etagBytes, err := p.meta.RetrieveAttribute(nil, bucket, object, etagkey)
|
|
if err == nil || errors.Is(err, fs.ErrNotExist) || errors.Is(err, meta.ErrNoSuchKey) {
|
|
return backend.EvaluateObjectPutPreconditions(string(etagBytes), ifMatch, ifNoneMatch, err == nil)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// snapshotObjVersion copies the current object at bucket/key into the
|
|
// versioning directory before the object is replaced, if versioning is
|
|
// configured for the bucket and there is an object to snapshot.
|
|
func (p *Posix) snapshotObjVersion(bucket, key string, vStatus types.BucketVersioningStatus, acct auth.Account) error {
|
|
if !p.versioningEnabled() || vStatus == "" {
|
|
return nil
|
|
}
|
|
|
|
isDir := strings.HasSuffix(key, "/")
|
|
d, err := os.Stat(p.ObjectPath(bucket, key))
|
|
if err != nil || d.IsDir() != isDir {
|
|
// nothing to snapshot
|
|
return nil
|
|
}
|
|
if isDir {
|
|
isObj, err := p.isDirObject(bucket, key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !isObj {
|
|
// nothing to snapshot
|
|
return nil
|
|
}
|
|
}
|
|
|
|
var isVersionIdMissing bool
|
|
if p.isBucketVersioningSuspended(vStatus) {
|
|
vIdBytes, err := p.meta.RetrieveAttribute(nil, bucket, key, versionIdKey)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return fmt.Errorf("get object versionId: %w", err)
|
|
}
|
|
isVersionIdMissing = len(vIdBytes) == 0
|
|
}
|
|
if !isVersionIdMissing {
|
|
_, err := p.createObjVersion(bucket, key, d.Size(), acct, false)
|
|
if err != nil {
|
|
return fmt.Errorf("create object version: %w", err)
|
|
}
|
|
// With path-based metadata backends (e.g. sidecar), object-lock
|
|
// attributes written on the previous version persist at this path
|
|
// after createObjVersion because metadata is not replaced atomically
|
|
// the way xattrs are on file rename. Delete them so they do not
|
|
// bleed into the new version.
|
|
_ = p.meta.DeleteAttribute(bucket, key, objectLegalHoldKey)
|
|
_ = p.meta.DeleteAttribute(bucket, key, objectRetentionKey)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Posix) PutObject(ctx context.Context, po s3response.PutObjectInput) (s3response.PutObjectOutput, error) {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, err
|
|
}
|
|
defer release()
|
|
|
|
return p.PutObjectWithPostFunc(ctx, po, func(*os.File) error { return nil })
|
|
}
|
|
|
|
func (p *Posix) PutObjectWithPostFunc(ctx context.Context, po s3response.PutObjectInput, postprocess func(f *os.File) error) (s3response.PutObjectOutput, error) {
|
|
acct, ok := ctx.Value("account").(auth.Account)
|
|
if !ok {
|
|
acct = auth.Account{}
|
|
}
|
|
|
|
if po.Key == nil {
|
|
return s3response.PutObjectOutput{}, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
if !p.isBucketValid(*po.Bucket) {
|
|
return s3response.PutObjectOutput{}, s3err.GetBucketErr(s3err.ErrInvalidBucketName, *po.Bucket)
|
|
}
|
|
err := p.doesBucketExist(*po.Bucket)
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, err
|
|
}
|
|
|
|
tags, err := backend.ParseObjectTags(getString(po.Tagging))
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, err
|
|
}
|
|
|
|
name := p.ObjectPath(*po.Bucket, *po.Key)
|
|
|
|
// Fast-fail precondition check before the request body is staged. This
|
|
// is only advisory: the authoritative check is repeated while holding
|
|
// the object publish lock at commit time so that evaluation and
|
|
// publication are atomic under concurrent writers.
|
|
err = p.checkPutPreconditions(*po.Bucket, *po.Key, po.IfMatch, po.IfNoneMatch)
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, err
|
|
}
|
|
|
|
uid, gid, doChown := p.getChownIDs(acct)
|
|
|
|
contentLength := int64(0)
|
|
if po.ContentLength != nil {
|
|
contentLength = *po.ContentLength
|
|
}
|
|
|
|
chRdr, chunkUpload := po.Body.(middlewares.ChecksumReader)
|
|
isTrailingChecksum := chunkUpload && chRdr.Algorithm() != ""
|
|
|
|
checksumAlgorithm := po.ChecksumAlgorithm
|
|
checksumValue := ""
|
|
|
|
if isTrailingChecksum {
|
|
checksumAlgorithm = types.ChecksumAlgorithm(strings.ToUpper(chRdr.Algorithm()))
|
|
checksumValue = chRdr.Checksum()
|
|
} else {
|
|
hashConfigs := []hashConfig{
|
|
{po.ChecksumCRC32, utils.HashTypeCRC32},
|
|
{po.ChecksumCRC32C, utils.HashTypeCRC32C},
|
|
{po.ChecksumSHA1, utils.HashTypeSha1},
|
|
{po.ChecksumSHA256, utils.HashTypeSha256},
|
|
{po.ChecksumCRC64NVME, utils.HashTypeCRC64NVME},
|
|
{po.ChecksumSHA512, utils.HashTypeSha512},
|
|
{po.ChecksumMD5, utils.HashTypeMd5},
|
|
{po.ChecksumXXHASH64, utils.HashTypeXXHASH64},
|
|
{po.ChecksumXXHASH3, utils.HashTypeXXHASH3},
|
|
{po.ChecksumXXHASH128, utils.HashTypeXXHASH128},
|
|
}
|
|
|
|
for _, config := range hashConfigs {
|
|
if config.value != nil {
|
|
checksumAlgorithm = types.ChecksumAlgorithm(strings.ToUpper(string(config.hashType)))
|
|
checksumValue = *config.value
|
|
break
|
|
}
|
|
}
|
|
|
|
if checksumAlgorithm == "" {
|
|
// default to crc64nvme
|
|
checksumAlgorithm = types.ChecksumAlgorithmCrc64nvme
|
|
}
|
|
}
|
|
|
|
if strings.HasSuffix(*po.Key, "/") {
|
|
// object is directory
|
|
if contentLength != 0 {
|
|
// posix directories can't contain data, send error
|
|
// if reuests has a data payload associated with a
|
|
// directory object
|
|
return s3response.PutObjectOutput{}, s3err.GetAPIError(s3err.ErrDirectoryObjectContainsData)
|
|
}
|
|
|
|
// serialize with competing writers to the same key and repeat the
|
|
// precondition check authoritatively under the publish lock
|
|
unlock, err := p.lockObjectPublish(ctx, *po.Bucket, *po.Key)
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, err
|
|
}
|
|
defer unlock()
|
|
|
|
err = p.checkPutPreconditions(*po.Bucket, *po.Key, po.IfMatch, po.IfNoneMatch)
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, err
|
|
}
|
|
|
|
expectedSum := getEmptyChecksumValue(checksumAlgorithm)
|
|
if checksumValue != "" && expectedSum != checksumValue {
|
|
return s3response.PutObjectOutput{}, s3err.GetChecksumBadDigestErr(checksumAlgorithm)
|
|
}
|
|
|
|
// reject object lock settings the bucket doesn't support before
|
|
// the directory object is changed
|
|
if po.ObjectLockLegalHoldStatus == types.ObjectLockLegalHoldStatusOn || po.ObjectLockMode != "" {
|
|
err = p.isBucketObjectLockEnabled(*po.Bucket)
|
|
if errors.Is(err, s3err.GetAPIError(s3err.ErrMissingObjectLockConfiguration)) {
|
|
return s3response.PutObjectOutput{}, s3err.GetAPIError(s3err.ErrMissingObjectLockConfigurationNoSpaces)
|
|
}
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, err
|
|
}
|
|
}
|
|
|
|
vStatus, err := p.getBucketVersioningStatus(ctx, *po.Bucket)
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, err
|
|
}
|
|
versioned := p.versioningEnabled() && vStatus != ""
|
|
|
|
// In a versioned bucket the directory is kept across versions: its
|
|
// current version is copied to the versioning directory and its
|
|
// object attributes are then replaced with the new version's.
|
|
err = p.snapshotObjVersion(*po.Bucket, *po.Key, vStatus, acct)
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, err
|
|
}
|
|
|
|
err = p.mkdirAll(name, uid, gid, doChown)
|
|
if err != nil {
|
|
if errors.Is(err, syscall.EDQUOT) {
|
|
return s3response.PutObjectOutput{}, s3err.GetAPIError(s3err.ErrQuotaExceeded)
|
|
}
|
|
if errors.Is(err, syscall.ENOSPC) {
|
|
return s3response.PutObjectOutput{}, s3err.GetAPIError(s3err.ErrNoSpaceLeftOnDevice)
|
|
}
|
|
return s3response.PutObjectOutput{}, err
|
|
}
|
|
|
|
var versionID string
|
|
if versioned {
|
|
err = p.clearDirObjectAttrs(*po.Bucket, *po.Key)
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, err
|
|
}
|
|
|
|
if p.isBucketVersioningSuspended(vStatus) {
|
|
err = p.deleteNullVersionIdObject(*po.Bucket, *po.Key)
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, err
|
|
}
|
|
} else {
|
|
versionID = ulid.Make().String()
|
|
}
|
|
}
|
|
|
|
err = p.storeObjectMetadata(nil, *po.Bucket, *po.Key, po.Metadata)
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, fmt.Errorf("set object metadata: %w", err)
|
|
}
|
|
|
|
dirETag := emptyMD5
|
|
if p.dataIntegrityEtag {
|
|
dirETag = fmt.Sprintf("\"%s-%s\"", strings.ToUpper(string(checksumAlgorithm)), expectedSum)
|
|
}
|
|
|
|
// set etag attribute to signify this dir was specifically put
|
|
err = p.meta.StoreAttribute(nil, *po.Bucket, *po.Key, etagkey,
|
|
[]byte(dirETag))
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, fmt.Errorf("set etag attr: %w", err)
|
|
}
|
|
|
|
// set "application/x-directory" content-type
|
|
err = p.meta.StoreAttribute(nil, *po.Bucket, *po.Key, contentTypeHdr,
|
|
[]byte(backend.DirContentType))
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, fmt.Errorf("set content-type attr: %w", err)
|
|
}
|
|
|
|
if getString(po.WebsiteRedirectLocation) != "" {
|
|
err = p.meta.StoreAttribute(nil, *po.Bucket, *po.Key, websiteRedirectHdr,
|
|
[]byte(*po.WebsiteRedirectLocation))
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, fmt.Errorf("set website-redirect-location attr: %w", err)
|
|
}
|
|
}
|
|
|
|
// set empty checksum
|
|
checksum := s3response.Checksum{
|
|
Type: types.ChecksumTypeFullObject,
|
|
Algorithm: checksumAlgorithm,
|
|
}
|
|
|
|
checksum.SetSum(checksumAlgorithm, &expectedSum)
|
|
|
|
err = p.storeChecksums(nil, *po.Bucket, *po.Key, checksum)
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, fmt.Errorf("store checksum: %w", err)
|
|
}
|
|
|
|
if versionID != "" {
|
|
err = p.meta.StoreAttribute(nil, *po.Bucket, *po.Key, versionIdKey, []byte(versionID))
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, fmt.Errorf("set versionId attr: %w", err)
|
|
}
|
|
}
|
|
|
|
if versioned {
|
|
// The directory mtime is the version's last modified time.
|
|
// Setting it needs the directory to be owned by the gateway,
|
|
// otherwise it's left as is.
|
|
now := time.Now()
|
|
_ = os.Chtimes(name, now, now)
|
|
}
|
|
|
|
// Set object tagging once the etag makes the directory an object
|
|
if tags != nil {
|
|
err := p.PutObjectTagging(withCtxNoSlot(ctx), *po.Bucket, *po.Key, "", tags)
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, err
|
|
}
|
|
}
|
|
|
|
err = p.putObjectLockSettings(ctx, po)
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, err
|
|
}
|
|
|
|
return s3response.PutObjectOutput{
|
|
ETag: dirETag,
|
|
VersionID: versionID,
|
|
Size: &contentLength,
|
|
ChecksumType: checksum.Type,
|
|
ChecksumCRC32: checksum.CRC32,
|
|
ChecksumCRC32C: checksum.CRC32C,
|
|
ChecksumCRC64NVME: checksum.CRC64NVME,
|
|
ChecksumSHA1: checksum.SHA1,
|
|
ChecksumSHA256: checksum.SHA256,
|
|
ChecksumSHA512: checksum.SHA512,
|
|
ChecksumMD5: checksum.MD5,
|
|
ChecksumXXHASH64: checksum.XXHASH64,
|
|
ChecksumXXHASH3: checksum.XXHASH3,
|
|
ChecksumXXHASH128: checksum.XXHASH128,
|
|
}, nil
|
|
}
|
|
|
|
vStatus, err := p.getBucketVersioningStatus(ctx, *po.Bucket)
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, err
|
|
}
|
|
vEnabled := p.isBucketVersioningEnabled(vStatus)
|
|
|
|
// object is file
|
|
d, err := os.Stat(name)
|
|
if err == nil && d.IsDir() {
|
|
return s3response.PutObjectOutput{}, s3err.GetAPIError(s3err.ErrExistingObjectIsDirectory)
|
|
}
|
|
|
|
// Version snapshots are taken under the object publish lock below. This
|
|
// keeps concurrent versioned overwrites ordered with their publications.
|
|
conditional := po.IfMatch != nil || po.IfNoneMatch != nil
|
|
if isErrNameTooLong(err) {
|
|
return s3response.PutObjectOutput{}, s3err.GetKeyTooLongErr(int64(len(*po.Key)), 1024)
|
|
}
|
|
if isErrNotDir(err) {
|
|
parentErr := handleParentDirError(name)
|
|
if parentErr != nil {
|
|
return s3response.PutObjectOutput{}, parentErr
|
|
}
|
|
}
|
|
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
return s3response.PutObjectOutput{}, fmt.Errorf("stat object: %w", err)
|
|
}
|
|
|
|
f, err := p.openTmpFile(p.ObjectPath(*po.Bucket, MetaTmpDir),
|
|
*po.Bucket, *po.Key, contentLength, acct, doFalloc, p.forceNoTmpFile, odirectAllowed)
|
|
if err != nil {
|
|
if errors.Is(err, syscall.EDQUOT) {
|
|
drainBody(po.Body)
|
|
return s3response.PutObjectOutput{}, s3err.GetAPIError(s3err.ErrQuotaExceeded)
|
|
}
|
|
if errors.Is(err, syscall.ENOSPC) {
|
|
drainBody(po.Body)
|
|
return s3response.PutObjectOutput{}, s3err.GetAPIError(s3err.ErrNoSpaceLeftOnDevice)
|
|
}
|
|
return s3response.PutObjectOutput{}, fmt.Errorf("open temp file: %w", err)
|
|
}
|
|
defer f.cleanup()
|
|
|
|
objsize := f.size
|
|
|
|
// When dataIntegrityEtag is enabled the MD5 is never used, so skip
|
|
// both the allocating md5.New() and the per-byte TeeReader overhead.
|
|
var md5hash hash.Hash
|
|
var rdr io.Reader
|
|
if p.dataIntegrityEtag {
|
|
rdr = po.Body
|
|
} else {
|
|
md5hash = md5.New()
|
|
rdr = io.TeeReader(po.Body, md5hash)
|
|
}
|
|
|
|
var hashRdr *utils.HashReader
|
|
if !isTrailingChecksum {
|
|
hashRdr, err = utils.NewHashReader(rdr, checksumValue, utils.HashType(strings.ToLower(string(checksumAlgorithm))))
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, fmt.Errorf("initialize hash reader: %w", err)
|
|
}
|
|
|
|
rdr = hashRdr
|
|
}
|
|
|
|
buf := p.getIOBuffer()
|
|
defer p.putIOBuffer(buf)
|
|
|
|
_, err = io.CopyBuffer(f, rdr, buf)
|
|
if err != nil {
|
|
if errors.Is(err, syscall.EDQUOT) {
|
|
drainBody(rdr)
|
|
return s3response.PutObjectOutput{}, s3err.GetAPIError(s3err.ErrQuotaExceeded)
|
|
}
|
|
if errors.Is(err, syscall.ENOSPC) {
|
|
drainBody(rdr)
|
|
return s3response.PutObjectOutput{}, s3err.GetAPIError(s3err.ErrNoSpaceLeftOnDevice)
|
|
}
|
|
// Return the error itself, if it implements the s3err.S3Error interface
|
|
if _, ok := err.(s3err.S3Error); ok {
|
|
return s3response.PutObjectOutput{}, err
|
|
}
|
|
return s3response.PutObjectOutput{}, fmt.Errorf("write object data: %w", err)
|
|
}
|
|
|
|
// If the file was pre-allocated (via fallocate) to a size larger than the
|
|
// bytes actually written, truncate it to the real content size. This can
|
|
// happen when Content-Length includes epilogue bytes after the multipart
|
|
// final boundary (e.g. browser-based POST Object with trailing data).
|
|
if f.size > 0 {
|
|
objsize -= f.size
|
|
if err := f.File().Truncate(objsize); err != nil {
|
|
return s3response.PutObjectOutput{}, fmt.Errorf("truncate object to actual size: %w", err)
|
|
}
|
|
}
|
|
|
|
dir := filepath.Dir(name)
|
|
if dir != "" {
|
|
err = p.mkdirAll(dir, uid, gid, doChown)
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, s3err.GetAPIError(s3err.ErrExistingObjectIsDirectory)
|
|
}
|
|
}
|
|
|
|
// if the versioning is enabled, generate a new versionID for the object
|
|
var versionID string
|
|
if p.versioningEnabled() && vEnabled {
|
|
versionID = ulid.Make().String()
|
|
}
|
|
|
|
// Run the backend post-process hook on the staged file before taking
|
|
// the publish lock and before any path-based metadata is written, so
|
|
// that a failing hook neither holds up other writers nor leaves
|
|
// partial metadata behind.
|
|
err = postprocess(f.File())
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{},
|
|
fmt.Errorf("put object post process failed: %w", err)
|
|
}
|
|
|
|
// The body is fully staged; serialize the commit phase (condition
|
|
// evaluation, metadata stores, and final link) with competing writers
|
|
// to the same key.
|
|
unlock, err := p.lockObjectPublish(ctx, *po.Bucket, *po.Key)
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, err
|
|
}
|
|
defer unlock()
|
|
|
|
if conditional {
|
|
// authoritative precondition check under the publish lock
|
|
err = p.checkPutPreconditions(*po.Bucket, *po.Key, po.IfMatch, po.IfNoneMatch)
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, err
|
|
}
|
|
|
|
}
|
|
|
|
// Snapshot the object that is about to be replaced only after the
|
|
// conditional check has succeeded and while holding the publish lock.
|
|
verr := p.snapshotObjVersion(*po.Bucket, *po.Key, vStatus, acct)
|
|
if verr != nil {
|
|
return s3response.PutObjectOutput{}, verr
|
|
}
|
|
|
|
// Marker cleanup belongs to publishing: snapshotting can be skipped for
|
|
// orphaned sidecars, suspended versioning, or a missing version ID.
|
|
err = p.meta.DeleteAttribute(*po.Bucket, *po.Key, deleteMarkerKey)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) && !errors.Is(err, fs.ErrNotExist) {
|
|
return s3response.PutObjectOutput{}, fmt.Errorf("delete object delete-marker: %w", err)
|
|
}
|
|
|
|
// Before finalizing the object creation remove
|
|
// null versionId object from versioning directory
|
|
// if it exists and the versioning status is Suspended
|
|
if p.isBucketVersioningSuspended(vStatus) {
|
|
err = p.deleteNullVersionIdObject(*po.Bucket, *po.Key)
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, err
|
|
}
|
|
versionID = nullVersionId
|
|
// Clear any stale versionId sidecar attribute left from a previous
|
|
// versioned object at this path. With xattr this is implicit (the
|
|
// new file carries only the attrs set on the tmpfile), but with
|
|
// path-based metadata the old attr persists until explicitly deleted.
|
|
_ = p.meta.DeleteAttribute(*po.Bucket, *po.Key, versionIdKey)
|
|
}
|
|
|
|
var sum string
|
|
|
|
if isTrailingChecksum {
|
|
sum = chRdr.Checksum()
|
|
} else {
|
|
sum = hashRdr.Sum()
|
|
}
|
|
|
|
// Generate the ETag for this object.
|
|
// When dataIntegrityEtag is enabled, embed the checksum algorithm name and
|
|
// value directly in the ETag (e.g. "CRC64NVME-<base64>") instead of the
|
|
// standard MD5 digest. The checksum is always available here: either
|
|
// supplied by the client or computed internally as CRC64NVME by default.
|
|
var etag string
|
|
if p.dataIntegrityEtag {
|
|
etag = fmt.Sprintf("\"%s-%s\"", strings.ToUpper(string(checksumAlgorithm)), sum)
|
|
} else {
|
|
etag = backend.GenerateEtag(md5hash)
|
|
}
|
|
|
|
checksum := s3response.Checksum{
|
|
Type: types.ChecksumTypeFullObject,
|
|
Algorithm: checksumAlgorithm,
|
|
}
|
|
|
|
checksum.SetSum(checksumAlgorithm, &sum)
|
|
err = p.storeChecksums(f.File(), *po.Bucket, *po.Key, checksum)
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, fmt.Errorf("store checksum: %w", err)
|
|
}
|
|
|
|
err = p.meta.StoreAttribute(f.File(), *po.Bucket, *po.Key, etagkey, []byte(etag))
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, fmt.Errorf("set etag attr: %w", err)
|
|
}
|
|
|
|
err = p.storeObjectMetaProperties(f.File(), *po.Bucket, *po.Key,
|
|
metaProperties{
|
|
ContentType: po.ContentType,
|
|
ContentEncoding: po.ContentEncoding,
|
|
ContentLanguage: po.ContentLanguage,
|
|
ContentDisposition: po.ContentDisposition,
|
|
CacheControl: po.CacheControl,
|
|
Expires: po.Expires,
|
|
WebsiteRedirectLocation: po.WebsiteRedirectLocation,
|
|
Metadata: po.Metadata,
|
|
})
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, err
|
|
}
|
|
|
|
if versionID != "" && versionID != nullVersionId {
|
|
err := p.meta.StoreAttribute(f.File(), *po.Bucket, *po.Key, versionIdKey, []byte(versionID))
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, fmt.Errorf("set versionId attr: %w", err)
|
|
}
|
|
}
|
|
|
|
// "null" identifies the current object internally while versioning is
|
|
// suspended, but S3 omits the version ID from the PutObject response.
|
|
if versionID == nullVersionId {
|
|
versionID = ""
|
|
}
|
|
|
|
err = f.link()
|
|
if errors.Is(err, syscall.EEXIST) {
|
|
return s3response.PutObjectOutput{
|
|
ETag: etag,
|
|
VersionID: versionID,
|
|
}, nil
|
|
}
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, s3err.GetAPIError(s3err.ErrExistingObjectIsDirectory)
|
|
}
|
|
|
|
// Set object tagging
|
|
if tags != nil {
|
|
err := p.PutObjectTagging(withCtxNoSlot(ctx), *po.Bucket, *po.Key, "", tags)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return s3response.PutObjectOutput{
|
|
ETag: etag,
|
|
VersionID: versionID,
|
|
}, nil
|
|
}
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, err
|
|
}
|
|
}
|
|
|
|
err = p.putObjectLockSettings(ctx, po)
|
|
if err != nil {
|
|
return s3response.PutObjectOutput{}, err
|
|
}
|
|
|
|
return s3response.PutObjectOutput{
|
|
ETag: etag,
|
|
VersionID: versionID,
|
|
ChecksumCRC32: checksum.CRC32,
|
|
ChecksumCRC32C: checksum.CRC32C,
|
|
ChecksumSHA1: checksum.SHA1,
|
|
ChecksumSHA256: checksum.SHA256,
|
|
ChecksumCRC64NVME: checksum.CRC64NVME,
|
|
ChecksumSHA512: checksum.SHA512,
|
|
ChecksumMD5: checksum.MD5,
|
|
ChecksumXXHASH64: checksum.XXHASH64,
|
|
ChecksumXXHASH3: checksum.XXHASH3,
|
|
ChecksumXXHASH128: checksum.XXHASH128,
|
|
Size: &objsize,
|
|
ChecksumType: checksum.Type,
|
|
}, nil
|
|
}
|
|
|
|
// putObjectLockSettings sets the legal hold and retention requested with
|
|
// the put on the object that was just published
|
|
func (p *Posix) putObjectLockSettings(ctx context.Context, po s3response.PutObjectInput) error {
|
|
// Set object legal hold
|
|
if po.ObjectLockLegalHoldStatus == types.ObjectLockLegalHoldStatusOn {
|
|
err := p.PutObjectLegalHold(withCtxNoSlot(ctx), *po.Bucket, *po.Key, "", true)
|
|
if err != nil {
|
|
if errors.Is(err, s3err.GetAPIError(s3err.ErrMissingObjectLockConfiguration)) {
|
|
err = s3err.GetAPIError(s3err.ErrMissingObjectLockConfigurationNoSpaces)
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Set object retention
|
|
if po.ObjectLockMode != "" {
|
|
retention := types.ObjectLockRetention{
|
|
Mode: types.ObjectLockRetentionMode(po.ObjectLockMode),
|
|
RetainUntilDate: po.ObjectLockRetainUntilDate,
|
|
}
|
|
retParsed, err := json.Marshal(retention)
|
|
if err != nil {
|
|
return fmt.Errorf("parse object lock retention: %w", err)
|
|
}
|
|
err = p.PutObjectRetention(withCtxNoSlot(ctx), *po.Bucket, *po.Key, "", retParsed)
|
|
if err != nil {
|
|
if errors.Is(err, s3err.GetAPIError(s3err.ErrMissingObjectLockConfiguration)) {
|
|
err = s3err.GetAPIError(s3err.ErrMissingObjectLockConfigurationNoSpaces)
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Posix) DeleteObject(ctx context.Context, input *s3.DeleteObjectInput) (*s3.DeleteObjectOutput, error) {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer release()
|
|
|
|
if input.Key == nil {
|
|
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
|
|
bucket := *input.Bucket
|
|
object := *input.Key
|
|
isDir := strings.HasSuffix(object, "/")
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return nil, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
|
|
if err := p.validateVersionId(backend.GetStringFromPtr(input.VersionId)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
objpath := p.ObjectPath(bucket, object)
|
|
|
|
vStatus, err := p.getBucketVersioningStatus(ctx, bucket)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
evalPreconditions := func(f os.FileInfo, bucket, object string) error {
|
|
var err error
|
|
if f == nil {
|
|
f, err = os.Stat(p.ObjectPath(bucket, object))
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
b, err := p.meta.RetrieveAttribute(nil, bucket, object, etagkey)
|
|
etag := string(b)
|
|
if err != nil || etag == "" {
|
|
etag = p.defaultEtag
|
|
}
|
|
|
|
// evaluate preconditions
|
|
return backend.EvaluateObjectDeletePreconditions(etag, f.ModTime(), f.Size(),
|
|
backend.ObjectDeletePreconditions{
|
|
IfMatch: input.IfMatch,
|
|
IfMatchLastModTime: input.IfMatchLastModifiedTime,
|
|
IfMatchSize: input.IfMatchSize,
|
|
})
|
|
}
|
|
|
|
if p.versioningEnabled() && vStatus != "" {
|
|
if getString(input.VersionId) == "" {
|
|
// if the versionId is not specified, make the current version a delete marker
|
|
fi, err := os.Stat(objpath)
|
|
if errors.Is(err, fs.ErrNotExist) || isErrNotDir(err) {
|
|
// AWS returns success if the object does not exist
|
|
return &s3.DeleteObjectOutput{}, nil
|
|
}
|
|
if isErrNameTooLong(err) {
|
|
return nil, s3err.GetKeyTooLongErr(int64(len(object)), 1024)
|
|
}
|
|
if err != nil {
|
|
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
// the entry at the object path may be the object of the key
|
|
// with or without the trailing slash
|
|
isObj, err := p.isLiveObject(fi, bucket, object)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !isObj {
|
|
// AWS returns success if the object does not exist
|
|
return &s3.DeleteObjectOutput{}, nil
|
|
}
|
|
|
|
err = evalPreconditions(fi, bucket, object)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
acct, ok := ctx.Value("account").(auth.Account)
|
|
if !ok {
|
|
acct = auth.Account{}
|
|
}
|
|
|
|
// Get object versionId
|
|
vId, err := p.meta.RetrieveAttribute(nil, bucket, object, versionIdKey)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) && !errors.Is(err, fs.ErrNotExist) {
|
|
return nil, fmt.Errorf("get obj versionId: %w", err)
|
|
}
|
|
if errors.Is(err, meta.ErrNoSuchKey) {
|
|
vId = []byte(nullVersionId)
|
|
}
|
|
|
|
// Creates a new object version in the versioning directory
|
|
if p.isBucketVersioningEnabled(vStatus) || string(vId) != nullVersionId {
|
|
_, err = p.createObjVersion(bucket, object, fi.Size(), acct, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Mark the object as a delete marker
|
|
err = p.meta.StoreAttribute(nil, bucket, object, deleteMarkerKey, []byte{})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("set delete marker: %w", err)
|
|
}
|
|
|
|
versionId := nullVersionId
|
|
if p.isBucketVersioningEnabled(vStatus) {
|
|
// Generate & set a unique versionId for the delete marker
|
|
versionId = ulid.Make().String()
|
|
err = p.meta.StoreAttribute(nil, bucket, object, versionIdKey, []byte(versionId))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("set versionId: %w", err)
|
|
}
|
|
} else {
|
|
err = p.meta.DeleteAttribute(bucket, object, versionIdKey)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return nil, fmt.Errorf("delete versionId: %w", err)
|
|
}
|
|
}
|
|
|
|
return &s3.DeleteObjectOutput{
|
|
DeleteMarker: getBoolPtr(true),
|
|
VersionId: &versionId,
|
|
}, nil
|
|
} else {
|
|
versionPath := p.genObjVersionPath(bucket, object)
|
|
|
|
// the attributes at the object path may belong to the key with
|
|
// or without the trailing slash, or to a directory that isn't an
|
|
// object
|
|
_, err := p.statLiveObject(bucket, object)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
// AWS returns success if the object does not exist
|
|
return &s3.DeleteObjectOutput{VersionId: input.VersionId}, nil
|
|
}
|
|
if isErrNameTooLong(err) {
|
|
return nil, s3err.GetKeyTooLongErr(int64(len(object)), 1024)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("stat object: %w", err)
|
|
}
|
|
|
|
vId, err := p.meta.RetrieveAttribute(nil, bucket, object, versionIdKey)
|
|
if errors.Is(err, fs.ErrNotExist) || isErrNotDir(err) {
|
|
// AWS returns success if the object does not exist
|
|
return &s3.DeleteObjectOutput{
|
|
VersionId: input.VersionId,
|
|
}, nil
|
|
}
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return nil, fmt.Errorf("get obj versionId: %w", err)
|
|
}
|
|
if errors.Is(err, meta.ErrNoSuchKey) {
|
|
vId = []byte(nullVersionId)
|
|
}
|
|
|
|
if string(vId) == *input.VersionId {
|
|
// evaluate preconditions
|
|
err := evalPreconditions(nil, bucket, object)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// if the specified VersionId is the same as in the latest version,
|
|
// remove the latest version, find the latest version from the versioning
|
|
// directory and move to the place of the deleted object, to make it the latest
|
|
isDelMarker, err := p.isObjDeleteMarker(bucket, object)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if isDir {
|
|
err = p.deleteDirObjectLatestVersion(bucket, object)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &s3.DeleteObjectOutput{
|
|
DeleteMarker: &isDelMarker,
|
|
VersionId: input.VersionId,
|
|
}, nil
|
|
}
|
|
err = os.Remove(objpath)
|
|
if err != nil && !errors.Is(err, fs.ErrNotExist) && !isErrNotDir(err) {
|
|
return nil, fmt.Errorf("remove obj version: %w", err)
|
|
}
|
|
|
|
ents, err := os.ReadDir(versionPath)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
if err := p.meta.DeleteAttributes(bucket, object); err != nil &&
|
|
!errors.Is(err, meta.ErrNoSuchKey) && !errors.Is(err, fs.ErrNotExist) {
|
|
return nil, fmt.Errorf("delete object attributes: %w", err)
|
|
}
|
|
p.removeParents(bucket, object)
|
|
return &s3.DeleteObjectOutput{
|
|
DeleteMarker: &isDelMarker,
|
|
VersionId: input.VersionId,
|
|
}, nil
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read version dir: %w", err)
|
|
}
|
|
|
|
if len(ents) == 0 {
|
|
if err := p.meta.DeleteAttributes(bucket, object); err != nil &&
|
|
!errors.Is(err, meta.ErrNoSuchKey) && !errors.Is(err, fs.ErrNotExist) {
|
|
return nil, fmt.Errorf("delete object attributes: %w", err)
|
|
}
|
|
p.removeParents(bucket, object)
|
|
return &s3.DeleteObjectOutput{
|
|
DeleteMarker: &isDelMarker,
|
|
VersionId: input.VersionId,
|
|
}, nil
|
|
}
|
|
|
|
srcObjVersion, err := latestObjVersion(ents).Info()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get file info: %w", err)
|
|
}
|
|
srcVersionId := srcObjVersion.Name()
|
|
sf, err := os.Open(filepath.Join(versionPath, srcVersionId))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open obj version: %w", err)
|
|
}
|
|
acct, ok := ctx.Value("account").(auth.Account)
|
|
if !ok {
|
|
acct = auth.Account{}
|
|
}
|
|
|
|
f, err := p.openTmpFile(p.ObjectPath(bucket, MetaTmpDir),
|
|
bucket, object, srcObjVersion.Size(), acct, doFalloc,
|
|
p.forceNoTmpFile, odirectNotAllowed)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open tmp file: %w", err)
|
|
}
|
|
defer f.cleanup()
|
|
|
|
// Prioritize copy_file_range for internal file-to-file version restores.
|
|
_, err = io.Copy(f.File(), sf)
|
|
if err != nil {
|
|
_ = sf.Close()
|
|
return nil, fmt.Errorf("copy object %w", err)
|
|
}
|
|
|
|
if err := sf.Close(); err != nil {
|
|
return nil, fmt.Errorf("close obj version: %w", err)
|
|
}
|
|
|
|
if err := f.link(); err != nil {
|
|
return nil, fmt.Errorf("link tmp file: %w", err)
|
|
}
|
|
|
|
// With path-based metadata (sidecar) the live object's attrs are
|
|
// not replaced atomically. The restored version may not have all
|
|
// the attrs that the deleted version had (e.g. a null version has
|
|
// no versionIdKey). Clear attrs that belong to the deleted version
|
|
// before copying the restored version's attrs so that the restored
|
|
// version presents a clean state.
|
|
_ = p.meta.DeleteAttribute(bucket, object, versionIdKey)
|
|
_ = p.meta.DeleteAttribute(bucket, object, deleteMarkerKey)
|
|
|
|
attrs, err := p.meta.ListAttributes(versionPath, srcVersionId)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list object attributes: %w", err)
|
|
}
|
|
|
|
for _, attr := range attrs {
|
|
data, err := p.meta.RetrieveAttribute(nil, versionPath, srcVersionId, attr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load %v attribute", attr)
|
|
}
|
|
|
|
err = p.meta.StoreAttribute(nil, bucket, object, attr, data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("store %v attribute", attr)
|
|
}
|
|
}
|
|
|
|
err = os.Remove(filepath.Join(versionPath, srcVersionId))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("remove obj version %w", err)
|
|
}
|
|
|
|
_ = p.meta.DeleteAttributes(versionPath, srcVersionId)
|
|
p.removeParents(filepath.Join(p.versioningDir, bucket), filepath.Join(genObjVersionKey(object), *input.VersionId))
|
|
|
|
return &s3.DeleteObjectOutput{
|
|
DeleteMarker: &isDelMarker,
|
|
VersionId: input.VersionId,
|
|
}, nil
|
|
}
|
|
|
|
err = evalPreconditions(nil, versionPath, *input.VersionId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
isDelMarker, _ := p.isObjDeleteMarker(versionPath, *input.VersionId)
|
|
|
|
err = os.Remove(filepath.Join(versionPath, *input.VersionId))
|
|
if isErrNameTooLong(err) {
|
|
return nil, s3err.GetKeyTooLongErr(int64(len(object)), 1024)
|
|
}
|
|
if errors.Is(err, fs.ErrNotExist) || isErrNotDir(err) {
|
|
return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, *input.VersionId)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("delete object: %w", err)
|
|
}
|
|
|
|
_ = p.meta.DeleteAttributes(versionPath, *input.VersionId)
|
|
p.removeParents(filepath.Join(p.versioningDir, bucket), filepath.Join(genObjVersionKey(object), *input.VersionId))
|
|
|
|
return &s3.DeleteObjectOutput{
|
|
DeleteMarker: &isDelMarker,
|
|
VersionId: input.VersionId,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
fi, err := os.Stat(objpath)
|
|
if isErrNameTooLong(err) {
|
|
return nil, s3err.GetKeyTooLongErr(int64(len(object)), 1024)
|
|
}
|
|
if errors.Is(err, fs.ErrNotExist) || isErrNotDir(err) {
|
|
// AWS returns success if the object does not exist
|
|
return &s3.DeleteObjectOutput{}, nil
|
|
}
|
|
if err != nil {
|
|
return &s3.DeleteObjectOutput{}, fmt.Errorf("stat object: %w", err)
|
|
}
|
|
if strings.HasSuffix(object, "/") && !fi.IsDir() {
|
|
// requested object is expecting a directory with a trailing
|
|
// slash, but the object is not a directory. treat this as
|
|
// a non-existent object.
|
|
// AWS returns success if the object does not exist
|
|
return &s3.DeleteObjectOutput{}, nil
|
|
}
|
|
if !strings.HasSuffix(object, "/") && fi.IsDir() {
|
|
// requested object is expecting a file, but the object is a
|
|
// directory. treat this as a non-existent object.
|
|
// AWS returns success if the object does not exist
|
|
return &s3.DeleteObjectOutput{}, nil
|
|
}
|
|
|
|
err = evalPreconditions(fi, bucket, object)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = os.Remove(objpath)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
if isErrDirNotEmpty(err) {
|
|
// If the directory object has been uploaded explicitly
|
|
// remove the directory object (remove the ETag)
|
|
_, err = p.meta.RetrieveAttribute(nil, bucket, object, etagkey)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return nil, fmt.Errorf("get object etag: %w", err)
|
|
}
|
|
if errors.Is(err, meta.ErrNoSuchKey) {
|
|
return nil, s3err.GetAPIError(s3err.ErrDirectoryNotEmpty)
|
|
}
|
|
|
|
err = p.meta.DeleteAttribute(bucket, object, etagkey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("delete object etag: %w", err)
|
|
}
|
|
|
|
return &s3.DeleteObjectOutput{}, nil
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("delete object: %w", err)
|
|
}
|
|
|
|
err = p.meta.DeleteAttributes(bucket, object)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("delete object attributes: %w", err)
|
|
}
|
|
|
|
p.removeParents(bucket, object)
|
|
|
|
return &s3.DeleteObjectOutput{}, nil
|
|
}
|
|
|
|
// latestObjVersion returns the entry of the version, among the version
|
|
// directory entries, that becomes the latest one when the latest version
|
|
// of the object is deleted
|
|
func latestObjVersion(ents []fs.DirEntry) fs.DirEntry {
|
|
return ents[len(ents)-1]
|
|
}
|
|
|
|
// deleteDirObjectLatestVersion removes the latest version of the directory
|
|
// object at bucket/key. The newest remaining version is restored onto the
|
|
// directory. With no versions left the directory stops being an object and
|
|
// is removed, unless other objects are under it.
|
|
func (p *Posix) deleteDirObjectLatestVersion(bucket, key string) error {
|
|
objpath := p.ObjectPath(bucket, key)
|
|
versionPath := p.genObjVersionPath(bucket, key)
|
|
|
|
ents, err := os.ReadDir(versionPath)
|
|
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
return fmt.Errorf("read version dir: %w", err)
|
|
}
|
|
|
|
if len(ents) == 0 {
|
|
err := os.Remove(objpath)
|
|
if isErrDirNotEmpty(err) {
|
|
// the directory stays as the parent of the objects under it
|
|
err = p.clearDirObjectAttrs(bucket, key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = p.meta.DeleteAttribute(bucket, key, etagkey)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return fmt.Errorf("remove etag attribute: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
return fmt.Errorf("remove dir object: %w", err)
|
|
}
|
|
err = p.meta.DeleteAttributes(bucket, key)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) && !errors.Is(err, fs.ErrNotExist) {
|
|
return fmt.Errorf("delete object attributes: %w", err)
|
|
}
|
|
p.removeParents(bucket, key)
|
|
return nil
|
|
}
|
|
|
|
srcVersion := latestObjVersion(ents)
|
|
srcVersionId := srcVersion.Name()
|
|
srcInfo, err := srcVersion.Info()
|
|
if err != nil {
|
|
return fmt.Errorf("get file info: %w", err)
|
|
}
|
|
|
|
// replace the attributes in place, so that the directory keeps its etag
|
|
for _, attr := range dirObjectAttrs {
|
|
data, err := p.meta.RetrieveAttribute(nil, versionPath, srcVersionId, attr)
|
|
if errors.Is(err, meta.ErrNoSuchKey) {
|
|
err = p.meta.DeleteAttribute(bucket, key, attr)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return fmt.Errorf("remove %v attribute: %w", attr, err)
|
|
}
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("load %v attribute: %w", attr, err)
|
|
}
|
|
|
|
err = p.meta.StoreAttribute(nil, bucket, key, attr, data)
|
|
if err != nil {
|
|
return fmt.Errorf("store %v attribute: %w", attr, err)
|
|
}
|
|
}
|
|
|
|
err = os.Remove(filepath.Join(versionPath, srcVersionId))
|
|
if err != nil {
|
|
return fmt.Errorf("remove obj version: %w", err)
|
|
}
|
|
|
|
_ = p.meta.DeleteAttributes(versionPath, srcVersionId)
|
|
p.removeParents(filepath.Join(p.versioningDir, bucket), filepath.Join(genObjVersionKey(key), srcVersionId))
|
|
|
|
// The restored version keeps its last modified time. Setting it needs
|
|
// the directory to be owned by the gateway, otherwise it's left as is.
|
|
_ = os.Chtimes(objpath, time.Now(), srcInfo.ModTime())
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Posix) removeParents(bucket, object string) {
|
|
// this will remove all parent directories that were not
|
|
// specifically uploaded with a put object. we detect
|
|
// this with a special attribute to indicate these. stop
|
|
// at either the bucket or the first parent we encounter
|
|
// with the attribute, whichever comes first.
|
|
|
|
// Remove the last path separator for the directory objects
|
|
// to correctly detect the parent in the loop
|
|
objPath := strings.TrimSuffix(object, "/")
|
|
for {
|
|
parent := filepath.Dir(objPath)
|
|
if parent == string(filepath.Separator) || parent == "." {
|
|
// stop removing parents if we hit the bucket directory.
|
|
break
|
|
}
|
|
|
|
_, err := p.meta.RetrieveAttribute(nil, bucket, parent, etagkey)
|
|
if err == nil {
|
|
// a directory with a valid etag means this was specifically
|
|
// uploaded with a put object, so stop here and leave this
|
|
// directory in place.
|
|
break
|
|
}
|
|
|
|
err = os.Remove(p.ObjectPath(bucket, parent))
|
|
if err != nil {
|
|
break
|
|
}
|
|
|
|
objPath = parent
|
|
}
|
|
}
|
|
|
|
func (p *Posix) DeleteObjects(ctx context.Context, input *s3.DeleteObjectsInput) (s3response.DeleteResult, error) {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return s3response.DeleteResult{}, err
|
|
}
|
|
defer release()
|
|
|
|
bucket := *input.Bucket
|
|
if !p.isBucketValid(bucket) {
|
|
return s3response.DeleteResult{}, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
// a missing bucket fails the whole request rather than each key
|
|
err = p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return s3response.DeleteResult{}, err
|
|
}
|
|
|
|
delResult, errs := []types.DeletedObject{}, []types.Error{}
|
|
for _, obj := range input.Delete.Objects {
|
|
//TODO: Make the delete operation concurrent
|
|
// once concurrency is implemented, the posix rate limiter should
|
|
// be taken into account.
|
|
res, err := p.DeleteObject(withCtxNoSlot(ctx), &s3.DeleteObjectInput{
|
|
Bucket: input.Bucket,
|
|
Key: obj.Key,
|
|
VersionId: obj.VersionId,
|
|
})
|
|
if err == nil {
|
|
delEntity := types.DeletedObject{
|
|
Key: obj.Key,
|
|
DeleteMarker: res.DeleteMarker,
|
|
VersionId: obj.VersionId,
|
|
}
|
|
if delEntity.DeleteMarker != nil && *delEntity.DeleteMarker {
|
|
delEntity.DeleteMarkerVersionId = res.VersionId
|
|
}
|
|
|
|
delResult = append(delResult, delEntity)
|
|
} else {
|
|
errs = append(errs, s3err.ObjectDeleteError(obj.Key, obj.VersionId, err))
|
|
}
|
|
}
|
|
|
|
return s3response.DeleteResult{
|
|
Deleted: delResult,
|
|
Error: errs,
|
|
}, nil
|
|
}
|
|
|
|
func (p *Posix) GetObject(ctx context.Context, input *s3.GetObjectInput) (*s3.GetObjectOutput, error) {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer release()
|
|
|
|
if input.Key == nil {
|
|
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
var versionId string
|
|
if input.VersionId != nil {
|
|
versionId = *input.VersionId
|
|
}
|
|
|
|
if err := p.validateVersionId(versionId); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !p.versioningEnabled() && versionId != "" {
|
|
//TODO: Maybe we need to return our custom error here?
|
|
return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, versionId)
|
|
}
|
|
|
|
bucket := *input.Bucket
|
|
if !p.isBucketValid(bucket) {
|
|
return nil, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
err = p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
object := *input.Key
|
|
if versionId != "" {
|
|
vId, err := p.meta.RetrieveAttribute(nil, bucket, object, versionIdKey)
|
|
if errors.Is(err, fs.ErrNotExist) || isErrNotDir(err) {
|
|
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return nil, fmt.Errorf("get obj versionId: %w", err)
|
|
}
|
|
if errors.Is(err, meta.ErrNoSuchKey) {
|
|
vId = []byte(nullVersionId)
|
|
}
|
|
|
|
if string(vId) != versionId {
|
|
bucket = filepath.Join(p.versioningDir, bucket)
|
|
object = filepath.Join(genObjVersionKey(object), versionId)
|
|
}
|
|
}
|
|
|
|
objPath := p.ObjectPath(bucket, object)
|
|
|
|
fid, err := os.Stat(objPath)
|
|
if errors.Is(err, fs.ErrNotExist) || isErrNotDir(err) {
|
|
if versionId != "" {
|
|
return nil, s3err.GetNoSuchVersionErr(*input.Key, versionId)
|
|
}
|
|
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
if isErrNameTooLong(err) {
|
|
return nil, s3err.GetKeyTooLongErr(int64(len(*input.Key)), 1024)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("stat object: %w", err)
|
|
}
|
|
|
|
if strings.HasSuffix(object, "/") && !fid.IsDir() {
|
|
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
if !strings.HasSuffix(object, "/") && fid.IsDir() {
|
|
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
if fid.IsDir() {
|
|
// Only directories explicitly created via S3 (put-object with key ending
|
|
// in '/') have an etag attribute. Directories created incidentally on the
|
|
// filesystem or as parent directories during object upload should not be
|
|
// accessible via get-object.
|
|
isObj, err := p.isDirObject(bucket, object)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !isObj {
|
|
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
}
|
|
|
|
if p.versioningEnabled() {
|
|
isDelMarker, err := p.isObjDeleteMarker(bucket, object)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// if the specified object version is a delete marker, return MethodNotAllowed
|
|
if isDelMarker {
|
|
if versionId != "" {
|
|
err = s3err.GetAPIError(s3err.ErrMethodNotAllowed)
|
|
} else {
|
|
err = s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
return &s3.GetObjectOutput{
|
|
DeleteMarker: getBoolPtr(true),
|
|
LastModified: backend.GetTimePtr(fid.ModTime()),
|
|
}, err
|
|
}
|
|
}
|
|
|
|
b, err := p.meta.RetrieveAttribute(nil, bucket, object, etagkey)
|
|
etag := string(b)
|
|
if err != nil || etag == "" {
|
|
etag = p.defaultEtag
|
|
}
|
|
|
|
// evaluate preconditions
|
|
err = backend.EvaluatePreconditions(etag, fid.ModTime(), backend.PreConditions{
|
|
IfMatch: input.IfMatch,
|
|
IfNoneMatch: input.IfNoneMatch,
|
|
IfModSince: input.IfModifiedSince,
|
|
IfUnmodeSince: input.IfUnmodifiedSince,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If versioning is configured get the object versionId
|
|
if p.versioningEnabled() && versionId == "" {
|
|
vId, err := p.meta.RetrieveAttribute(nil, bucket, object, versionIdKey)
|
|
if errors.Is(err, meta.ErrNoSuchKey) {
|
|
versionId = nullVersionId
|
|
} else if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
versionId = string(vId)
|
|
}
|
|
|
|
if fid.IsDir() {
|
|
_, _, _, err := backend.ParseObjectRange(0, *input.Range)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
objMeta := p.loadObjectMetaProperties(nil, bucket, object, &fid)
|
|
|
|
var tagCount *int32
|
|
tags, err := p.getAttrTags(bucket, object, versionId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(tags) != 0 {
|
|
tgCount := int32(len(tags))
|
|
tagCount = &tgCount
|
|
}
|
|
|
|
var checksums s3response.Checksum
|
|
if input.ChecksumMode == types.ChecksumModeEnabled {
|
|
checksums, err = p.retrieveChecksums(nil, bucket, object)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return nil, fmt.Errorf("get object checksums: %w", err)
|
|
}
|
|
}
|
|
|
|
var length int64 = 0
|
|
return &s3.GetObjectOutput{
|
|
ChecksumCRC32: checksums.CRC32,
|
|
ChecksumCRC32C: checksums.CRC32C,
|
|
ChecksumSHA1: checksums.SHA1,
|
|
ChecksumSHA256: checksums.SHA256,
|
|
ChecksumCRC64NVME: checksums.CRC64NVME,
|
|
ChecksumSHA512: checksums.SHA512,
|
|
ChecksumMD5: checksums.MD5,
|
|
ChecksumXXHASH64: checksums.XXHASH64,
|
|
ChecksumXXHASH3: checksums.XXHASH3,
|
|
ChecksumXXHASH128: checksums.XXHASH128,
|
|
ChecksumType: checksums.Type,
|
|
AcceptRanges: backend.GetPtrFromString("bytes"),
|
|
ContentLength: &length,
|
|
ContentEncoding: objMeta.ContentEncoding,
|
|
ContentType: objMeta.ContentType,
|
|
ContentLanguage: objMeta.ContentLanguage,
|
|
ContentDisposition: objMeta.ContentDisposition,
|
|
CacheControl: objMeta.CacheControl,
|
|
ExpiresString: objMeta.Expires,
|
|
WebsiteRedirectLocation: objMeta.WebsiteRedirectLocation,
|
|
ETag: &etag,
|
|
LastModified: backend.GetTimePtr(fid.ModTime()),
|
|
Metadata: objMeta.Metadata,
|
|
TagCount: tagCount,
|
|
ContentRange: nil,
|
|
StorageClass: types.StorageClassStandard,
|
|
VersionId: &versionId,
|
|
}, nil
|
|
}
|
|
|
|
// openForRead opens with FILE_SHARE_DELETE on Windows so that a concurrent
|
|
// DeleteObject can call os.Remove on this file while the GET response body
|
|
// is still being streamed. On POSIX, os.Open is sufficient.
|
|
f, err := openForRead(objPath, p.enableODirect)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("open object: %w", err)
|
|
}
|
|
|
|
fi, err := f.Stat()
|
|
if errors.Is(err, fs.ErrNotExist) || isErrNotDir(err) {
|
|
if versionId != "" {
|
|
return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, versionId)
|
|
}
|
|
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
if isErrNameTooLong(err) {
|
|
return nil, s3err.GetKeyTooLongErr(int64(len(*input.Key)), 1024)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("stat object: %w", err)
|
|
}
|
|
|
|
objSize := fi.Size()
|
|
if fi.IsDir() {
|
|
objSize = 0
|
|
}
|
|
|
|
var contentRange *string
|
|
var startOffset, length int64
|
|
var partsCount *int32
|
|
|
|
// If partNumber is requested and mp-metadata exists, serve that specific part.
|
|
// For non-multipart objects (no mp-metadata), partNumber=1 returns the full
|
|
// object with no Content-Range; any other partNumber is out of range.
|
|
// Both range read and partNumber can't be used together.
|
|
if input.PartNumber != nil {
|
|
mpMetaBytes, metaErr := p.meta.RetrieveAttribute(nil, bucket, object, mpMetaKey)
|
|
if metaErr == nil {
|
|
mpMeta, err := backend.UnmarshalMpUploadMetadata(mpMetaBytes, false)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse object multipart metadata: %w", err)
|
|
}
|
|
|
|
partNum := *input.PartNumber
|
|
totalParts := int32(len(mpMeta.Parts))
|
|
partsCount = &totalParts
|
|
if partNum > totalParts {
|
|
return nil, s3err.GetInvalidPartNumberRangeErr(totalParts, partNum)
|
|
}
|
|
|
|
// Parts holds cumulative sizes: Parts[i] = sum of sizes 1..i+1
|
|
if partNum > 1 {
|
|
startOffset = mpMeta.Parts[partNum-2]
|
|
}
|
|
length = mpMeta.Parts[partNum-1] - startOffset
|
|
contentRange = backend.GetPtrFromString(fmt.Sprintf("bytes %d-%d/%d", startOffset, startOffset+length-1, objSize))
|
|
} else if errors.Is(metaErr, meta.ErrNoSuchKey) {
|
|
// Non-multipart object: partNumber=1 means the whole object; anything
|
|
// higher is out of range
|
|
if *input.PartNumber > 1 {
|
|
return nil, s3err.GetInvalidPartNumberRangeErr(1, *input.PartNumber)
|
|
}
|
|
length = objSize
|
|
if objSize != 0 {
|
|
// if object size is 0, the whole object is served, no content range should be set
|
|
contentRange = backend.GetPtrFromString(fmt.Sprintf("bytes 0-%d/%d", objSize-1, objSize))
|
|
}
|
|
} else {
|
|
return nil, fmt.Errorf("retrieve mp metadata: %w", metaErr)
|
|
}
|
|
} else {
|
|
start, lgth, isValid, err := backend.ParseObjectRange(objSize, getString(input.Range))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
startOffset, length = start, lgth
|
|
|
|
if isValid {
|
|
contentRange = backend.GetPtrFromString(fmt.Sprintf("bytes %v-%v/%v", start, start+lgth-1, objSize))
|
|
}
|
|
}
|
|
|
|
objMeta := p.loadObjectMetaProperties(f, bucket, object, &fi)
|
|
|
|
var tagCount *int32
|
|
tags, err := p.getAttrTags(bucket, object, versionId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(tags) != 0 {
|
|
tgCount := int32(len(tags))
|
|
tagCount = &tgCount
|
|
}
|
|
|
|
var checksums s3response.Checksum
|
|
// Return checksums only when the full object is requested
|
|
if input.ChecksumMode == types.ChecksumModeEnabled && length-startOffset == objSize {
|
|
checksums, err = p.retrieveChecksums(f, bucket, object)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return nil, fmt.Errorf("get object checksums: %w", err)
|
|
}
|
|
}
|
|
|
|
// Full-object responses can keep the underlying *os.File for sendfile.
|
|
// Linux range reads on O_DIRECT may need runtime fallback to buffered I/O.
|
|
body, err := buildGetObjectBody(f, objPath, startOffset, length, objSize, p.enableODirect, p.ioBufferSize)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("build get object body: %w", err)
|
|
}
|
|
|
|
return &s3.GetObjectOutput{
|
|
AcceptRanges: backend.GetPtrFromString("bytes"),
|
|
ContentLength: &length,
|
|
ContentEncoding: objMeta.ContentEncoding,
|
|
ContentType: objMeta.ContentType,
|
|
ContentDisposition: objMeta.ContentDisposition,
|
|
ContentLanguage: objMeta.ContentLanguage,
|
|
CacheControl: objMeta.CacheControl,
|
|
ExpiresString: objMeta.Expires,
|
|
WebsiteRedirectLocation: objMeta.WebsiteRedirectLocation,
|
|
ETag: &etag,
|
|
LastModified: backend.GetTimePtr(fi.ModTime()),
|
|
Metadata: objMeta.Metadata,
|
|
TagCount: tagCount,
|
|
ContentRange: contentRange,
|
|
StorageClass: types.StorageClassStandard,
|
|
VersionId: &versionId,
|
|
Body: body,
|
|
ChecksumCRC32: checksums.CRC32,
|
|
ChecksumCRC32C: checksums.CRC32C,
|
|
ChecksumSHA1: checksums.SHA1,
|
|
ChecksumSHA256: checksums.SHA256,
|
|
ChecksumCRC64NVME: checksums.CRC64NVME,
|
|
ChecksumSHA512: checksums.SHA512,
|
|
ChecksumMD5: checksums.MD5,
|
|
ChecksumXXHASH64: checksums.XXHASH64,
|
|
ChecksumXXHASH3: checksums.XXHASH3,
|
|
ChecksumXXHASH128: checksums.XXHASH128,
|
|
ChecksumType: checksums.Type,
|
|
PartsCount: partsCount,
|
|
}, nil
|
|
}
|
|
|
|
func (p *Posix) HeadObject(ctx context.Context, input *s3.HeadObjectInput) (*s3.HeadObjectOutput, error) {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer release()
|
|
|
|
if input.Key == nil {
|
|
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
versionId := backend.GetStringFromPtr(input.VersionId)
|
|
|
|
if err := p.validateVersionId(versionId); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !p.versioningEnabled() && versionId != "" {
|
|
//TODO: Maybe we need to return our custom error here?
|
|
return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, versionId)
|
|
}
|
|
|
|
bucket := *input.Bucket
|
|
object := *input.Key
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return nil, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
|
|
err = p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if versionId != "" {
|
|
vId, err := p.meta.RetrieveAttribute(nil, bucket, object, versionIdKey)
|
|
if errors.Is(err, fs.ErrNotExist) || isErrNotDir(err) {
|
|
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return nil, fmt.Errorf("get obj versionId: %w", err)
|
|
}
|
|
if errors.Is(err, meta.ErrNoSuchKey) {
|
|
bucket = filepath.Join(p.versioningDir, bucket)
|
|
object = filepath.Join(genObjVersionKey(object), versionId)
|
|
}
|
|
|
|
if string(vId) != versionId {
|
|
bucket = filepath.Join(p.versioningDir, bucket)
|
|
object = filepath.Join(genObjVersionKey(object), versionId)
|
|
}
|
|
}
|
|
|
|
objPath := p.ObjectPath(bucket, object)
|
|
|
|
fi, err := os.Stat(objPath)
|
|
if errors.Is(err, fs.ErrNotExist) || isErrNotDir(err) {
|
|
if versionId != "" {
|
|
return nil, s3err.GetNoSuchVersionErr(*input.Key, versionId)
|
|
}
|
|
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
if isErrNameTooLong(err) {
|
|
return nil, s3err.GetKeyTooLongErr(int64(len(*input.Key)), 1024)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("stat object: %w", err)
|
|
}
|
|
if strings.HasSuffix(object, "/") && !fi.IsDir() {
|
|
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
if !strings.HasSuffix(object, "/") && fi.IsDir() {
|
|
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
if fi.IsDir() {
|
|
// Only directories explicitly created via S3 (put-object with key ending
|
|
// in '/') have an etag attribute. Directories created incidentally on the
|
|
// filesystem or as parent directories during object upload should not be
|
|
// accessible via head-object.
|
|
isObj, err := p.isDirObject(bucket, object)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !isObj {
|
|
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
}
|
|
|
|
if p.versioningEnabled() {
|
|
isDelMarker, err := p.isObjDeleteMarker(bucket, object)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// if the specified object version is a delete marker, return MethodNotAllowed
|
|
if isDelMarker {
|
|
if versionId != "" {
|
|
return &s3.HeadObjectOutput{
|
|
DeleteMarker: getBoolPtr(true),
|
|
LastModified: backend.GetTimePtr(fi.ModTime()),
|
|
}, s3err.GetAPIError(s3err.ErrMethodNotAllowed)
|
|
} else {
|
|
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
}
|
|
}
|
|
|
|
if p.versioningEnabled() && versionId == "" {
|
|
vId, err := p.meta.RetrieveAttribute(nil, bucket, object, versionIdKey)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return nil, fmt.Errorf("get object versionId: %v", err)
|
|
}
|
|
|
|
versionId = string(vId)
|
|
}
|
|
|
|
objMeta := p.loadObjectMetaProperties(nil, bucket, object, &fi)
|
|
|
|
b, err := p.meta.RetrieveAttribute(nil, bucket, object, etagkey)
|
|
etag := string(b)
|
|
if err != nil || etag == "" {
|
|
etag = p.defaultEtag
|
|
}
|
|
|
|
// evaluate preconditions
|
|
err = backend.EvaluatePreconditions(etag, fi.ModTime(), backend.PreConditions{
|
|
IfMatch: input.IfMatch,
|
|
IfNoneMatch: input.IfNoneMatch,
|
|
IfModSince: input.IfModifiedSince,
|
|
IfUnmodeSince: input.IfUnmodifiedSince,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
size := fi.Size()
|
|
if fi.IsDir() {
|
|
size = 0
|
|
}
|
|
|
|
var contentRange *string
|
|
var startOffset, length int64
|
|
var partsCount *int32
|
|
|
|
// If partNumber is requested and mp-metadata exists, serve that specific part.
|
|
// For non-multipart objects (no mp-metadata), partNumber=1 returns the full
|
|
// object with no Content-Range; any other partNumber is out of range.
|
|
// Both range read and partNumber can't be used together.
|
|
if input.PartNumber != nil {
|
|
mpMetaBytes, metaErr := p.meta.RetrieveAttribute(nil, bucket, object, mpMetaKey)
|
|
if metaErr == nil {
|
|
mpMeta, err := backend.UnmarshalMpUploadMetadata(mpMetaBytes, false)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parse object multipart metadata: %w", err)
|
|
}
|
|
|
|
partNum := *input.PartNumber
|
|
totalParts := int32(len(mpMeta.Parts))
|
|
partsCount = &totalParts
|
|
if partNum > totalParts {
|
|
return nil, s3err.GetInvalidPartNumberRangeErr(totalParts, partNum)
|
|
}
|
|
|
|
// Parts holds cumulative sizes: Parts[i] = sum of sizes 1..i+1
|
|
if partNum > 1 {
|
|
startOffset = mpMeta.Parts[partNum-2]
|
|
}
|
|
length = mpMeta.Parts[partNum-1] - startOffset
|
|
contentRange = backend.GetPtrFromString(fmt.Sprintf("bytes %d-%d/%d", startOffset, startOffset+length-1, size))
|
|
} else if errors.Is(metaErr, meta.ErrNoSuchKey) {
|
|
// Non-multipart object: partNumber=1 means the whole object; anything
|
|
// higher is out of range
|
|
if *input.PartNumber > 1 {
|
|
return nil, s3err.GetInvalidPartNumberRangeErr(1, *input.PartNumber)
|
|
}
|
|
length = size
|
|
if length != 0 {
|
|
// if object size is 0, the whole object is served, no content range should be set
|
|
contentRange = backend.GetPtrFromString(fmt.Sprintf("bytes 0-%d/%d", length-1, length))
|
|
}
|
|
} else {
|
|
return nil, fmt.Errorf("retrieve mp metadata: %w", metaErr)
|
|
}
|
|
} else {
|
|
start, lgth, isValid, err := backend.ParseObjectRange(size, getString(input.Range))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
startOffset, length = start, lgth
|
|
if isValid {
|
|
contentRange = backend.GetPtrFromString(fmt.Sprintf("bytes %v-%v/%v", start, start+lgth-1, size))
|
|
}
|
|
}
|
|
|
|
var objectLockLegalHoldStatus types.ObjectLockLegalHoldStatus
|
|
status, err := p.GetObjectLegalHold(withCtxNoSlot(ctx), bucket, object, versionId)
|
|
if err == nil {
|
|
if *status {
|
|
objectLockLegalHoldStatus = types.ObjectLockLegalHoldStatusOn
|
|
} else {
|
|
objectLockLegalHoldStatus = types.ObjectLockLegalHoldStatusOff
|
|
}
|
|
}
|
|
|
|
var objectLockMode types.ObjectLockMode
|
|
var objectLockRetainUntilDate *time.Time
|
|
retention, err := p.GetObjectRetention(withCtxNoSlot(ctx), bucket, object, versionId)
|
|
if err == nil {
|
|
var config types.ObjectLockRetention
|
|
if err := json.Unmarshal(retention, &config); err == nil {
|
|
objectLockMode = types.ObjectLockMode(config.Mode)
|
|
objectLockRetainUntilDate = config.RetainUntilDate
|
|
}
|
|
}
|
|
|
|
var checksums s3response.Checksum
|
|
// Return checksums only when the full object is requested
|
|
if input.ChecksumMode == types.ChecksumModeEnabled && length-startOffset == size {
|
|
checksums, err = p.retrieveChecksums(nil, bucket, object)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return nil, fmt.Errorf("get object checksums: %w", err)
|
|
}
|
|
}
|
|
|
|
var tagCount *int32
|
|
tags, err := p.getAttrTags(bucket, object, versionId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(tags) != 0 {
|
|
tc := int32(len(tags))
|
|
tagCount = &tc
|
|
}
|
|
|
|
return &s3.HeadObjectOutput{
|
|
ContentLength: &length,
|
|
AcceptRanges: backend.GetPtrFromString("bytes"),
|
|
ContentRange: contentRange,
|
|
ContentType: objMeta.ContentType,
|
|
ContentEncoding: objMeta.ContentEncoding,
|
|
ContentDisposition: objMeta.ContentDisposition,
|
|
ContentLanguage: objMeta.ContentLanguage,
|
|
CacheControl: objMeta.CacheControl,
|
|
ExpiresString: objMeta.Expires,
|
|
WebsiteRedirectLocation: objMeta.WebsiteRedirectLocation,
|
|
ETag: &etag,
|
|
LastModified: backend.GetTimePtr(fi.ModTime()),
|
|
Metadata: objMeta.Metadata,
|
|
ObjectLockLegalHoldStatus: objectLockLegalHoldStatus,
|
|
ObjectLockMode: objectLockMode,
|
|
ObjectLockRetainUntilDate: objectLockRetainUntilDate,
|
|
StorageClass: types.StorageClassStandard,
|
|
VersionId: &versionId,
|
|
ChecksumCRC32: checksums.CRC32,
|
|
ChecksumCRC32C: checksums.CRC32C,
|
|
ChecksumSHA1: checksums.SHA1,
|
|
ChecksumSHA256: checksums.SHA256,
|
|
ChecksumCRC64NVME: checksums.CRC64NVME,
|
|
ChecksumSHA512: checksums.SHA512,
|
|
ChecksumMD5: checksums.MD5,
|
|
ChecksumXXHASH64: checksums.XXHASH64,
|
|
ChecksumXXHASH3: checksums.XXHASH3,
|
|
ChecksumXXHASH128: checksums.XXHASH128,
|
|
ChecksumType: checksums.Type,
|
|
TagCount: tagCount,
|
|
PartsCount: partsCount,
|
|
}, nil
|
|
}
|
|
|
|
func (p *Posix) GetObjectAttributes(ctx context.Context, input *s3.GetObjectAttributesInput) (s3response.GetObjectAttributesResponse, error) {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return s3response.GetObjectAttributesResponse{}, err
|
|
}
|
|
defer release()
|
|
|
|
data, err := p.HeadObject(withCtxNoSlot(ctx), &s3.HeadObjectInput{
|
|
Bucket: input.Bucket,
|
|
Key: input.Key,
|
|
VersionId: input.VersionId,
|
|
ChecksumMode: types.ChecksumModeEnabled,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, s3err.GetAPIError(s3err.ErrMethodNotAllowed)) && data != nil {
|
|
return s3response.GetObjectAttributesResponse{
|
|
DeleteMarker: data.DeleteMarker,
|
|
VersionId: data.VersionId,
|
|
}, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
|
|
return s3response.GetObjectAttributesResponse{}, err
|
|
}
|
|
|
|
return s3response.GetObjectAttributesResponse{
|
|
ETag: backend.TrimEtag(data.ETag),
|
|
ObjectSize: data.ContentLength,
|
|
StorageClass: data.StorageClass,
|
|
LastModified: data.LastModified,
|
|
VersionId: data.VersionId,
|
|
DeleteMarker: data.DeleteMarker,
|
|
Checksum: &types.Checksum{
|
|
ChecksumCRC32: data.ChecksumCRC32,
|
|
ChecksumCRC32C: data.ChecksumCRC32C,
|
|
ChecksumSHA1: data.ChecksumSHA1,
|
|
ChecksumSHA256: data.ChecksumSHA256,
|
|
ChecksumCRC64NVME: data.ChecksumCRC64NVME,
|
|
ChecksumSHA512: data.ChecksumSHA512,
|
|
ChecksumMD5: data.ChecksumMD5,
|
|
ChecksumXXHASH64: data.ChecksumXXHASH64,
|
|
ChecksumXXHASH3: data.ChecksumXXHASH3,
|
|
ChecksumXXHASH128: data.ChecksumXXHASH128,
|
|
ChecksumType: data.ChecksumType,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func (p *Posix) CopyObject(ctx context.Context, input s3response.CopyObjectInput) (s3response.CopyObjectOutput, error) {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return s3response.CopyObjectOutput{}, err
|
|
}
|
|
defer release()
|
|
|
|
if input.Key == nil {
|
|
return s3response.CopyObjectOutput{}, s3err.GetAPIError(s3err.ErrInvalidCopyDest)
|
|
}
|
|
if input.ExpectedBucketOwner == nil {
|
|
return s3response.CopyObjectOutput{}, s3err.GetAPIError(s3err.ErrInvalidRequest)
|
|
}
|
|
|
|
srcBucket, srcObject, srcVersionId, err := backend.ParseCopySource(*input.CopySource)
|
|
if err != nil {
|
|
return s3response.CopyObjectOutput{}, err
|
|
}
|
|
if err := p.validateVersionId(srcVersionId); err != nil {
|
|
return s3response.CopyObjectOutput{}, err
|
|
}
|
|
if !p.isBucketValid(srcBucket) {
|
|
return s3response.CopyObjectOutput{}, s3err.GetBucketErr(s3err.ErrInvalidBucketName, srcBucket)
|
|
}
|
|
dstBucket := *input.Bucket
|
|
dstObject := *input.Key
|
|
|
|
if !p.isBucketValid(dstBucket) {
|
|
return s3response.CopyObjectOutput{}, s3err.GetBucketErr(s3err.ErrInvalidBucketName, dstBucket)
|
|
}
|
|
|
|
err = p.doesBucketExist(srcBucket)
|
|
if err != nil {
|
|
return s3response.CopyObjectOutput{}, err
|
|
}
|
|
|
|
if input.ExpectedSourceBucketOwner != nil && *input.ExpectedSourceBucketOwner != "" {
|
|
aclData, err := p.meta.RetrieveAttribute(nil, srcBucket, "", aclkey)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return s3response.CopyObjectOutput{}, fmt.Errorf("get src bucket acl: %w", err)
|
|
}
|
|
srcAcl, err := auth.ParseACL(aclData)
|
|
if err != nil {
|
|
return s3response.CopyObjectOutput{}, err
|
|
}
|
|
if srcAcl.Owner != *input.ExpectedSourceBucketOwner {
|
|
return s3response.CopyObjectOutput{}, s3err.GetAPIError(s3err.ErrAccessDenied)
|
|
}
|
|
}
|
|
|
|
vStatus, err := p.getBucketVersioningStatus(ctx, srcBucket)
|
|
if err != nil {
|
|
return s3response.CopyObjectOutput{}, err
|
|
}
|
|
vEnabled := p.isBucketVersioningEnabled(vStatus)
|
|
|
|
origSrcObject := srcObject
|
|
if srcVersionId != "" {
|
|
if !p.versioningEnabled() || !vEnabled {
|
|
return s3response.CopyObjectOutput{}, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, srcVersionId)
|
|
}
|
|
vId, err := p.meta.RetrieveAttribute(nil, srcBucket, srcObject, versionIdKey)
|
|
if errors.Is(err, fs.ErrNotExist) || isErrNotDir(err) {
|
|
return s3response.CopyObjectOutput{}, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return s3response.CopyObjectOutput{}, fmt.Errorf("get src object version id: %w", err)
|
|
}
|
|
|
|
if string(vId) != srcVersionId {
|
|
srcBucket = joinPathWithTrailer(p.versioningDir, srcBucket)
|
|
srcObject = joinPathWithTrailer(genObjVersionKey(srcObject), srcVersionId)
|
|
}
|
|
}
|
|
|
|
err = p.doesBucketExist(dstBucket)
|
|
if err != nil {
|
|
return s3response.CopyObjectOutput{}, err
|
|
}
|
|
|
|
objPath := joinPathWithTrailer(p.BucketPath(srcBucket), srcObject)
|
|
f, err := os.Open(objPath)
|
|
if errors.Is(err, fs.ErrNotExist) || isErrNotDir(err) {
|
|
if p.versioningEnabled() && vEnabled {
|
|
return s3response.CopyObjectOutput{}, s3err.GetNoSuchVersionErr(origSrcObject, srcVersionId)
|
|
}
|
|
return s3response.CopyObjectOutput{}, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
if isErrNameTooLong(err) {
|
|
return s3response.CopyObjectOutput{}, s3err.GetKeyTooLongErr(int64(len(origSrcObject)), 1024)
|
|
}
|
|
if err != nil {
|
|
return s3response.CopyObjectOutput{}, fmt.Errorf("open object: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
fi, err := f.Stat()
|
|
if err != nil {
|
|
return s3response.CopyObjectOutput{}, fmt.Errorf("stat object: %w", err)
|
|
}
|
|
if strings.HasSuffix(srcObject, "/") && !fi.IsDir() {
|
|
return s3response.CopyObjectOutput{}, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
if !strings.HasSuffix(srcObject, "/") && fi.IsDir() {
|
|
return s3response.CopyObjectOutput{}, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
if err := p.checkCopySourceDeleteMarker(srcBucket, srcObject, srcVersionId); err != nil {
|
|
return s3response.CopyObjectOutput{}, err
|
|
}
|
|
// a directory object holds no data
|
|
srcSize := fi.Size()
|
|
var srcBody io.Reader = f
|
|
if fi.IsDir() {
|
|
isObj, err := p.isDirObject(srcBucket, srcObject)
|
|
if err != nil {
|
|
return s3response.CopyObjectOutput{}, err
|
|
}
|
|
if !isObj {
|
|
return s3response.CopyObjectOutput{}, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
srcSize = 0
|
|
srcBody = strings.NewReader("")
|
|
}
|
|
if srcSize > p.copyObjectThreshold {
|
|
return s3response.CopyObjectOutput{}, s3err.GetCopySourceObjectTooLargeErr(p.copyObjectThreshold)
|
|
}
|
|
|
|
b, err := p.meta.RetrieveAttribute(f, srcBucket, srcObject, etagkey)
|
|
srcEtag := string(b)
|
|
if err != nil || srcEtag == "" {
|
|
srcEtag = p.defaultEtag
|
|
}
|
|
|
|
err = backend.EvaluatePreconditions(srcEtag, fi.ModTime(), backend.PreConditions{
|
|
IfMatch: input.CopySourceIfMatch,
|
|
IfNoneMatch: input.CopySourceIfNoneMatch,
|
|
IfModSince: input.CopySourceIfModifiedSince,
|
|
IfUnmodeSince: input.CopySourceIfUnmodifiedSince,
|
|
})
|
|
if err != nil {
|
|
return s3response.CopyObjectOutput{}, err
|
|
}
|
|
|
|
var etag string
|
|
var version *string
|
|
var crc32 *string
|
|
var crc32c *string
|
|
var sha1 *string
|
|
var sha256 *string
|
|
var crc64nvme *string
|
|
var sha512 *string
|
|
var md5sum *string
|
|
var xxhash64 *string
|
|
var xxhash3 *string
|
|
var xxhash128 *string
|
|
var chType types.ChecksumType
|
|
|
|
dstObjdPath := joinPathWithTrailer(p.BucketPath(dstBucket), dstObject)
|
|
// A copy of an object onto itself is rejected unless it replaces the
|
|
// object metadata. Naming a source version makes it a regular copy.
|
|
selfCopy := dstObjdPath == objPath
|
|
if selfCopy && srcVersionId == "" &&
|
|
input.MetadataDirective == types.MetadataDirectiveCopy {
|
|
return s3response.CopyObjectOutput{}, s3err.GetAPIError(s3err.ErrInvalidCopyDest)
|
|
}
|
|
|
|
// In a versioned bucket a self copy creates a new version like any other
|
|
// write, so only unversioned buckets are rewritten in place.
|
|
versioned := p.versioningEnabled() && vStatus != ""
|
|
if selfCopy && !versioned {
|
|
// Delete the object metadata
|
|
err = p.meta.DeleteAttribute(dstBucket, dstObject, metadataHdr)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return s3response.CopyObjectOutput{}, fmt.Errorf("delete object metadata: %w", err)
|
|
}
|
|
|
|
checksums, err := p.retrieveChecksums(nil, dstBucket, dstObject)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return s3response.CopyObjectOutput{}, fmt.Errorf("get obj checksums: %w", err)
|
|
}
|
|
|
|
chType = checksums.Type
|
|
|
|
if input.ChecksumAlgorithm != "" {
|
|
// If a different checksum algorith is specified
|
|
// first caclculate and store the checksum
|
|
if checksums.Algorithm != input.ChecksumAlgorithm {
|
|
f, err := os.Open(dstObjdPath)
|
|
if err != nil {
|
|
return s3response.CopyObjectOutput{}, fmt.Errorf("open obj file: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
hashReader, err := utils.NewHashReader(f, "", utils.HashType(strings.ToLower(string(input.ChecksumAlgorithm))))
|
|
if err != nil {
|
|
return s3response.CopyObjectOutput{}, fmt.Errorf("initialize hash reader: %w", err)
|
|
}
|
|
|
|
_, err = hashReader.Read(nil)
|
|
if err != nil {
|
|
return s3response.CopyObjectOutput{}, fmt.Errorf("read err: %w", err)
|
|
}
|
|
|
|
checksums = s3response.Checksum{}
|
|
|
|
sum := hashReader.Sum()
|
|
switch hashReader.Type() {
|
|
case utils.HashTypeCRC32:
|
|
checksums.CRC32 = &sum
|
|
crc32 = &sum
|
|
case utils.HashTypeCRC32C:
|
|
checksums.CRC32C = &sum
|
|
crc32c = &sum
|
|
case utils.HashTypeSha1:
|
|
checksums.SHA1 = &sum
|
|
sha1 = &sum
|
|
case utils.HashTypeSha256:
|
|
checksums.SHA256 = &sum
|
|
sha256 = &sum
|
|
case utils.HashTypeCRC64NVME:
|
|
checksums.CRC64NVME = &sum
|
|
crc64nvme = &sum
|
|
case utils.HashTypeSha512:
|
|
checksums.SHA512 = &sum
|
|
sha512 = &sum
|
|
case utils.HashTypeMd5:
|
|
checksums.MD5 = &sum
|
|
md5sum = &sum
|
|
case utils.HashTypeXXHASH64:
|
|
checksums.XXHASH64 = &sum
|
|
xxhash64 = &sum
|
|
case utils.HashTypeXXHASH3:
|
|
checksums.XXHASH3 = &sum
|
|
xxhash3 = &sum
|
|
case utils.HashTypeXXHASH128:
|
|
checksums.XXHASH128 = &sum
|
|
xxhash128 = &sum
|
|
}
|
|
|
|
// If a new checksum is calculated, the checksum type
|
|
// should be FULL_OBJECT
|
|
chType = types.ChecksumTypeFullObject
|
|
|
|
err = p.storeChecksums(f, dstBucket, dstObject, checksums)
|
|
if err != nil {
|
|
return s3response.CopyObjectOutput{}, fmt.Errorf("store checksum: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
b, _ := p.meta.RetrieveAttribute(nil, dstBucket, dstObject, etagkey)
|
|
etag = string(b)
|
|
vId, _ := p.meta.RetrieveAttribute(nil, dstBucket, dstObject, versionIdKey)
|
|
if errors.Is(err, fs.ErrNotExist) || isErrNotDir(err) {
|
|
return s3response.CopyObjectOutput{}, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
version = backend.GetPtrFromString(string(vId))
|
|
|
|
// Store the provided object meta properties
|
|
err = p.storeObjectMetaProperties(nil, dstBucket, dstObject,
|
|
metaProperties{
|
|
ContentType: input.ContentType,
|
|
ContentEncoding: input.ContentEncoding,
|
|
ContentLanguage: input.ContentLanguage,
|
|
ContentDisposition: input.ContentDisposition,
|
|
CacheControl: input.CacheControl,
|
|
Expires: input.Expires,
|
|
WebsiteRedirectLocation: input.WebsiteRedirectLocation,
|
|
Metadata: input.Metadata,
|
|
})
|
|
if err != nil {
|
|
return s3response.CopyObjectOutput{}, err
|
|
}
|
|
// explicitly delete the website redirect location, as if it's not
|
|
// provided as CopyObject input, it should not be copied
|
|
if getString(input.WebsiteRedirectLocation) == "" {
|
|
err := p.meta.DeleteAttribute(dstBucket, dstObject, websiteRedirectHdr)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return s3response.CopyObjectOutput{}, fmt.Errorf("delete website-redirect-location: %w", err)
|
|
}
|
|
}
|
|
|
|
if input.TaggingDirective == types.TaggingDirectiveReplace {
|
|
tags, err := backend.ParseObjectTags(getString(input.Tagging))
|
|
if err != nil {
|
|
return s3response.CopyObjectOutput{}, err
|
|
}
|
|
|
|
err = p.PutObjectTagging(withCtxNoSlot(ctx), dstBucket, dstObject, "", tags)
|
|
if err != nil {
|
|
return s3response.CopyObjectOutput{}, err
|
|
}
|
|
}
|
|
} else {
|
|
contentLength := srcSize
|
|
|
|
checksums, err := p.retrieveChecksums(f, srcBucket, srcObject)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return s3response.CopyObjectOutput{}, fmt.Errorf("get obj checksum: %w", err)
|
|
}
|
|
|
|
// If any checksum algorithm is provided, replace, otherwise
|
|
// use the existing one
|
|
if input.ChecksumAlgorithm != "" {
|
|
checksums.Algorithm = input.ChecksumAlgorithm
|
|
}
|
|
|
|
// A self copy publishes the new version over the source path, which
|
|
// on Windows can't be renamed over while the source is still open.
|
|
// PutObject reads the body before publishing, so the handle is
|
|
// released as soon as the data has been staged.
|
|
if selfCopy {
|
|
srcBody = &closeOnEOFReader{r: srcBody, c: f}
|
|
}
|
|
|
|
putObjectInput := s3response.PutObjectInput{
|
|
Bucket: &dstBucket,
|
|
Key: &dstObject,
|
|
Body: srcBody,
|
|
ContentLength: &contentLength,
|
|
ChecksumAlgorithm: checksums.Algorithm,
|
|
ContentType: input.ContentType,
|
|
ContentEncoding: input.ContentEncoding,
|
|
ContentDisposition: input.ContentDisposition,
|
|
ContentLanguage: input.ContentLanguage,
|
|
CacheControl: input.CacheControl,
|
|
Expires: input.Expires,
|
|
WebsiteRedirectLocation: input.WebsiteRedirectLocation,
|
|
Metadata: input.Metadata,
|
|
ObjectLockRetainUntilDate: input.ObjectLockRetainUntilDate,
|
|
ObjectLockMode: input.ObjectLockMode,
|
|
ObjectLockLegalHoldStatus: input.ObjectLockLegalHoldStatus,
|
|
}
|
|
|
|
// load and pass the source object meta properties, if metadata directive is "COPY"
|
|
if input.MetadataDirective != types.MetadataDirectiveReplace {
|
|
metaProps := p.loadObjectMetaProperties(nil, srcBucket, srcObject, &fi)
|
|
putObjectInput.ContentEncoding = metaProps.ContentEncoding
|
|
putObjectInput.ContentDisposition = metaProps.ContentDisposition
|
|
putObjectInput.ContentLanguage = metaProps.ContentLanguage
|
|
putObjectInput.ContentType = metaProps.ContentType
|
|
putObjectInput.CacheControl = metaProps.CacheControl
|
|
putObjectInput.Expires = metaProps.Expires
|
|
putObjectInput.Metadata = metaProps.Metadata
|
|
}
|
|
|
|
// pass the input tagging to PutObject, if tagging directive is "REPLACE"
|
|
if input.TaggingDirective == types.TaggingDirectiveReplace {
|
|
putObjectInput.Tagging = input.Tagging
|
|
}
|
|
|
|
// read the source tagging before the destination is written, as a
|
|
// self copy replaces the source object's metadata
|
|
var srcTagging []byte
|
|
var hasSrcTagging bool
|
|
if input.TaggingDirective == types.TaggingDirectiveCopy {
|
|
tagging, err := p.meta.RetrieveAttribute(nil, srcBucket, srcObject, tagHdr)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return s3response.CopyObjectOutput{}, fmt.Errorf("get source object tagging: %w", err)
|
|
}
|
|
srcTagging, hasSrcTagging = tagging, err == nil
|
|
}
|
|
|
|
res, err := p.PutObject(withCtxNoSlot(ctx), putObjectInput)
|
|
if err != nil {
|
|
return s3response.CopyObjectOutput{}, err
|
|
}
|
|
|
|
// the source tagging is stored after the destination object creation,
|
|
// if tagging directive is "COPY"
|
|
if hasSrcTagging {
|
|
err := p.meta.StoreAttribute(nil, dstBucket, dstObject, tagHdr, srcTagging)
|
|
if err != nil {
|
|
return s3response.CopyObjectOutput{}, fmt.Errorf("set destination object tagging: %w", err)
|
|
}
|
|
}
|
|
|
|
etag = res.ETag
|
|
version = &res.VersionID
|
|
crc32 = res.ChecksumCRC32
|
|
crc32c = res.ChecksumCRC32C
|
|
sha1 = res.ChecksumSHA1
|
|
sha256 = res.ChecksumSHA256
|
|
crc64nvme = res.ChecksumCRC64NVME
|
|
sha512 = res.ChecksumSHA512
|
|
md5sum = res.ChecksumMD5
|
|
xxhash64 = res.ChecksumXXHASH64
|
|
xxhash3 = res.ChecksumXXHASH3
|
|
xxhash128 = res.ChecksumXXHASH128
|
|
chType = res.ChecksumType
|
|
}
|
|
|
|
fi, err = os.Stat(dstObjdPath)
|
|
if err != nil {
|
|
return s3response.CopyObjectOutput{}, fmt.Errorf("stat dst object: %w", err)
|
|
}
|
|
|
|
return s3response.CopyObjectOutput{
|
|
CopyObjectResult: &s3response.CopyObjectResult{
|
|
ETag: &etag,
|
|
LastModified: backend.GetTimePtr(fi.ModTime()),
|
|
ChecksumCRC32: crc32,
|
|
ChecksumCRC32C: crc32c,
|
|
ChecksumSHA1: sha1,
|
|
ChecksumSHA256: sha256,
|
|
ChecksumCRC64NVME: crc64nvme,
|
|
ChecksumSHA512: sha512,
|
|
ChecksumMD5: md5sum,
|
|
ChecksumXXHASH64: xxhash64,
|
|
ChecksumXXHASH3: xxhash3,
|
|
ChecksumXXHASH128: xxhash128,
|
|
ChecksumType: chType,
|
|
},
|
|
VersionId: version,
|
|
CopySourceVersionId: &srcVersionId,
|
|
}, nil
|
|
}
|
|
|
|
func (p *Posix) ListObjects(ctx context.Context, input *s3.ListObjectsInput) (s3response.ListObjectsResult, error) {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return s3response.ListObjectsResult{}, err
|
|
}
|
|
defer release()
|
|
|
|
return p.ListObjectsParametrized(ctx, input, p.FileToObj)
|
|
}
|
|
|
|
func (p *Posix) ListObjectsParametrized(ctx context.Context, input *s3.ListObjectsInput, customFileToObj func(string, bool) backend.GetObjFunc) (s3response.ListObjectsResult, error) {
|
|
bucket := *input.Bucket
|
|
prefix := ""
|
|
if input.Prefix != nil {
|
|
prefix = *input.Prefix
|
|
}
|
|
marker := ""
|
|
if input.Marker != nil {
|
|
marker = *input.Marker
|
|
}
|
|
delim := ""
|
|
if input.Delimiter != nil {
|
|
delim = *input.Delimiter
|
|
}
|
|
maxkeys := int32(0)
|
|
if input.MaxKeys != nil {
|
|
maxkeys = *input.MaxKeys
|
|
}
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return s3response.ListObjectsResult{}, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
|
|
err := p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return s3response.ListObjectsResult{}, err
|
|
}
|
|
|
|
fileSystem := os.DirFS(p.BucketPath(bucket))
|
|
results, err := backend.Walk(ctx, fileSystem, prefix, delim, marker, maxkeys,
|
|
customFileToObj(bucket, true), []string{MetaTmpDir})
|
|
if err != nil {
|
|
return s3response.ListObjectsResult{}, fmt.Errorf("walk %v: %w", bucket, err)
|
|
}
|
|
|
|
return s3response.ListObjectsResult{
|
|
CommonPrefixes: results.CommonPrefixes,
|
|
Contents: results.Objects,
|
|
Delimiter: backend.GetPtrFromString(delim),
|
|
Marker: backend.GetPtrFromString(marker),
|
|
NextMarker: backend.GetPtrFromString(results.NextMarker),
|
|
Prefix: backend.GetPtrFromString(prefix),
|
|
IsTruncated: &results.Truncated,
|
|
MaxKeys: &maxkeys,
|
|
Name: &bucket,
|
|
}, nil
|
|
}
|
|
|
|
func (p *Posix) FileToObj(bucket string, fetchOwner bool) backend.GetObjFunc {
|
|
if !p.isBucketValid(bucket) {
|
|
return func(string, fs.DirEntry) (s3response.Object, error) {
|
|
return s3response.Object{}, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
}
|
|
|
|
return func(path string, d fs.DirEntry) (s3response.Object, error) {
|
|
var owner *types.Owner
|
|
// Retrieve the object owner data from bucket ACL, if fetchOwner is true
|
|
// All the objects in the bucket are owned by the bucket owner
|
|
if fetchOwner {
|
|
aclJSON, err := p.meta.RetrieveAttribute(nil, bucket, "", aclkey)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return s3response.Object{}, fmt.Errorf("get bucket acl: %w", err)
|
|
}
|
|
|
|
acl, err := auth.ParseACL(aclJSON)
|
|
if err != nil {
|
|
return s3response.Object{}, err
|
|
}
|
|
|
|
owner = &types.Owner{
|
|
ID: &acl.Owner,
|
|
}
|
|
}
|
|
if d.IsDir() {
|
|
// a directory is listed only if it's a directory object
|
|
// check to see if this is a directory object by checking etag
|
|
etagBytes, err := p.meta.RetrieveAttribute(nil, bucket, path, etagkey)
|
|
if errors.Is(err, meta.ErrNoSuchKey) || errors.Is(err, fs.ErrNotExist) {
|
|
return s3response.Object{}, backend.ErrSkipObj
|
|
}
|
|
if err != nil {
|
|
return s3response.Object{}, fmt.Errorf("get etag: %w", err)
|
|
}
|
|
etag := string(etagBytes)
|
|
|
|
// If the directory object is a delete marker, skip
|
|
isDel, _ := p.isObjDeleteMarker(bucket, path)
|
|
if isDel {
|
|
return s3response.Object{}, backend.ErrSkipObj
|
|
}
|
|
|
|
fi, err := d.Info()
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return s3response.Object{}, backend.ErrSkipObj
|
|
}
|
|
if err != nil {
|
|
return s3response.Object{}, fmt.Errorf("get fileinfo: %w", err)
|
|
}
|
|
|
|
size := int64(0)
|
|
mtime := fi.ModTime()
|
|
|
|
return s3response.Object{
|
|
ETag: &etag,
|
|
Key: &path,
|
|
LastModified: &mtime,
|
|
Size: &size,
|
|
StorageClass: types.ObjectStorageClassStandard,
|
|
Owner: owner,
|
|
}, nil
|
|
}
|
|
|
|
// If the object is a delete marker, skip
|
|
isDel, _ := p.isObjDeleteMarker(bucket, path)
|
|
if isDel {
|
|
return s3response.Object{}, backend.ErrSkipObj
|
|
}
|
|
|
|
// Retrieve the object checksum algorithm
|
|
checksums, err := p.retrieveChecksums(nil, bucket, path)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return s3response.Object{}, backend.ErrSkipObj
|
|
}
|
|
|
|
// file object, get object info and fill out object data
|
|
etagBytes, err := p.meta.RetrieveAttribute(nil, bucket, path, etagkey)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return s3response.Object{}, backend.ErrSkipObj
|
|
}
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return s3response.Object{}, fmt.Errorf("get etag: %w", err)
|
|
}
|
|
// note: meta.ErrNoSuchKey will return etagBytes = []byte{}
|
|
// so this will just set etag to "" if its not already set
|
|
|
|
etag := string(etagBytes)
|
|
if etag == "" {
|
|
etag = p.defaultEtag
|
|
}
|
|
|
|
fi, err := d.Info()
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return s3response.Object{}, backend.ErrSkipObj
|
|
}
|
|
if err != nil {
|
|
return s3response.Object{}, fmt.Errorf("get fileinfo: %w", err)
|
|
}
|
|
|
|
size := fi.Size()
|
|
mtime := fi.ModTime()
|
|
|
|
return s3response.Object{
|
|
ETag: &etag,
|
|
Key: &path,
|
|
LastModified: &mtime,
|
|
Size: &size,
|
|
StorageClass: types.ObjectStorageClassStandard,
|
|
ChecksumAlgorithm: []types.ChecksumAlgorithm{checksums.Algorithm},
|
|
ChecksumType: checksums.Type,
|
|
Owner: owner,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
func (p *Posix) ListObjectsV2(ctx context.Context, input *s3.ListObjectsV2Input) (s3response.ListObjectsV2Result, error) {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return s3response.ListObjectsV2Result{}, err
|
|
}
|
|
defer release()
|
|
|
|
return p.ListObjectsV2Parametrized(ctx, input, p.FileToObj)
|
|
}
|
|
|
|
func (p *Posix) ListObjectsV2Parametrized(ctx context.Context, input *s3.ListObjectsV2Input, customFileToObj func(string, bool) backend.GetObjFunc) (s3response.ListObjectsV2Result, error) {
|
|
bucket := *input.Bucket
|
|
prefix := ""
|
|
if input.Prefix != nil {
|
|
prefix = *input.Prefix
|
|
}
|
|
marker := ""
|
|
if input.ContinuationToken != nil {
|
|
if input.StartAfter != nil {
|
|
marker = max(*input.StartAfter, *input.ContinuationToken)
|
|
} else {
|
|
marker = *input.ContinuationToken
|
|
}
|
|
}
|
|
delim := ""
|
|
if input.Delimiter != nil {
|
|
delim = *input.Delimiter
|
|
}
|
|
maxkeys := int32(0)
|
|
if input.MaxKeys != nil {
|
|
maxkeys = *input.MaxKeys
|
|
}
|
|
var fetchOwner bool
|
|
if input.FetchOwner != nil {
|
|
fetchOwner = *input.FetchOwner
|
|
}
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return s3response.ListObjectsV2Result{}, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
|
|
err := p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return s3response.ListObjectsV2Result{}, err
|
|
}
|
|
|
|
fileSystem := os.DirFS(p.BucketPath(bucket))
|
|
results, err := backend.Walk(ctx, fileSystem, prefix, delim, marker, maxkeys,
|
|
customFileToObj(bucket, fetchOwner), []string{MetaTmpDir})
|
|
if err != nil {
|
|
return s3response.ListObjectsV2Result{}, fmt.Errorf("walk %v: %w", bucket, err)
|
|
}
|
|
|
|
count := int32(len(results.Objects) + len(results.CommonPrefixes))
|
|
|
|
return s3response.ListObjectsV2Result{
|
|
CommonPrefixes: results.CommonPrefixes,
|
|
Contents: results.Objects,
|
|
IsTruncated: &results.Truncated,
|
|
MaxKeys: &maxkeys,
|
|
Name: &bucket,
|
|
KeyCount: &count,
|
|
Delimiter: backend.GetPtrFromString(delim),
|
|
ContinuationToken: backend.GetPtrFromString(marker),
|
|
NextContinuationToken: backend.GetPtrFromString(results.NextMarker),
|
|
Prefix: backend.GetPtrFromString(prefix),
|
|
StartAfter: backend.GetPtrFromString(*input.StartAfter),
|
|
}, nil
|
|
}
|
|
|
|
func (p *Posix) PutBucketAcl(ctx context.Context, bucket string, data []byte) error {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer release()
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
err = p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = p.meta.StoreAttribute(nil, bucket, "", aclkey, data)
|
|
if err != nil {
|
|
return fmt.Errorf("set acl: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Posix) GetBucketAcl(ctx context.Context, input *s3.GetBucketAclInput) ([]byte, error) {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer release()
|
|
|
|
if !p.isBucketValid(*input.Bucket) {
|
|
return nil, s3err.GetBucketErr(s3err.ErrInvalidBucketName, *input.Bucket)
|
|
}
|
|
err = p.doesBucketExist(*input.Bucket)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
b, err := p.meta.RetrieveAttribute(nil, *input.Bucket, "", aclkey)
|
|
if errors.Is(err, meta.ErrNoSuchKey) {
|
|
return []byte{}, nil
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get acl: %w", err)
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
func (p *Posix) PutBucketTagging(ctx context.Context, bucket string, tags map[string]string) error {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer release()
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
err = p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if tags == nil {
|
|
err = p.meta.DeleteAttribute(bucket, "", tagHdr)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return fmt.Errorf("remove tags: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
b, err := json.Marshal(tags)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal tags: %w", err)
|
|
}
|
|
|
|
err = p.meta.StoreAttribute(nil, bucket, "", tagHdr, b)
|
|
if err != nil {
|
|
return fmt.Errorf("set tags: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Posix) GetBucketTagging(ctx context.Context, bucket string) (map[string]string, error) {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer release()
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return nil, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
err = p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tags, err := p.getAttrTags(bucket, "", "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return tags, nil
|
|
}
|
|
|
|
func (p *Posix) DeleteBucketTagging(ctx context.Context, bucket string) error {
|
|
if !p.isBucketValid(bucket) {
|
|
return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
return p.PutBucketTagging(ctx, bucket, nil)
|
|
}
|
|
|
|
func (p *Posix) GetObjectTagging(ctx context.Context, bucket, object, versionId string) (map[string]string, error) {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer release()
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return nil, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
err = p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := p.validateVersionId(versionId); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if versionId != "" && !p.versioningEnabled() {
|
|
//TODO: Maybe we need to return our custom error here?
|
|
return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, versionId)
|
|
}
|
|
|
|
bucket, object, err = p.objVersionAttrPath(bucket, object, versionId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = p.ensureNotDeleteMarker(bucket, object, versionId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return p.getAttrTags(bucket, object, versionId)
|
|
}
|
|
|
|
func (p *Posix) getAttrTags(bucket, object, versionId string) (map[string]string, error) {
|
|
tags := make(map[string]string)
|
|
b, err := p.meta.RetrieveAttribute(nil, bucket, object, tagHdr)
|
|
if errors.Is(err, fs.ErrNotExist) || isErrNotDir(err) {
|
|
if versionId != "" {
|
|
return nil, s3err.GetNoSuchVersionErr(object, versionId)
|
|
}
|
|
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
if errors.Is(err, meta.ErrNoSuchKey) {
|
|
if object != "" {
|
|
// return empty tag set for object tagging
|
|
return tags, nil
|
|
}
|
|
return nil, s3err.GetBucketErr(s3err.ErrBucketTaggingNotFound, bucket)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get tags: %w", err)
|
|
}
|
|
|
|
err = json.Unmarshal(b, &tags)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unmarshal tags: %w", err)
|
|
}
|
|
|
|
return tags, nil
|
|
}
|
|
|
|
func (p *Posix) PutObjectTagging(ctx context.Context, bucket, object, versionId string, tags map[string]string) error {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer release()
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
err = p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := p.validateVersionId(versionId); err != nil {
|
|
return err
|
|
}
|
|
|
|
if versionId != "" && !p.versioningEnabled() {
|
|
//TODO: Maybe we need to return our custom error here?
|
|
return s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, versionId)
|
|
}
|
|
|
|
bucket, object, err = p.objVersionAttrPath(bucket, object, versionId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = p.ensureNotDeleteMarker(bucket, object, versionId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if tags == nil {
|
|
err = p.meta.DeleteAttribute(bucket, object, tagHdr)
|
|
if errors.Is(err, fs.ErrNotExist) || isErrNotDir(err) {
|
|
if versionId != "" {
|
|
return s3err.GetNoSuchVersionErr(object, versionId)
|
|
}
|
|
return s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
if errors.Is(err, meta.ErrNoSuchKey) {
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("remove tags: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
err = p.storeObjectTags(bucket, object, tags)
|
|
if errors.Is(err, fs.ErrNotExist) || isErrNotDir(err) {
|
|
if versionId != "" {
|
|
return s3err.GetNoSuchVersionErr(object, versionId)
|
|
}
|
|
return s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("set tags: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// storeObjectTags stores tags as the tagging attribute of bucket/object
|
|
func (p *Posix) storeObjectTags(bucket, object string, tags map[string]string) error {
|
|
b, err := json.Marshal(tags)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal tags: %w", err)
|
|
}
|
|
|
|
return p.meta.StoreAttribute(nil, bucket, object, tagHdr, b)
|
|
}
|
|
|
|
func (p *Posix) DeleteObjectTagging(ctx context.Context, bucket, object, versionId string) error {
|
|
if !p.isBucketValid(bucket) {
|
|
return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
return p.PutObjectTagging(ctx, bucket, object, versionId, nil)
|
|
}
|
|
|
|
func (p *Posix) PutBucketPolicy(ctx context.Context, bucket string, policy []byte) error {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer release()
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
err = p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if policy == nil {
|
|
err := p.meta.DeleteAttribute(bucket, "", policykey)
|
|
if err != nil {
|
|
if errors.Is(err, meta.ErrNoSuchKey) {
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("remove policy: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
err = p.meta.StoreAttribute(nil, bucket, "", policykey, policy)
|
|
if err != nil {
|
|
return fmt.Errorf("set policy: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Posix) GetBucketPolicy(ctx context.Context, bucket string) ([]byte, error) {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer release()
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return nil, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
err = p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
policy, err := p.meta.RetrieveAttribute(nil, bucket, "", policykey)
|
|
if errors.Is(err, meta.ErrNoSuchKey) {
|
|
return nil, s3err.GetBucketErr(s3err.ErrNoSuchBucketPolicy, bucket)
|
|
}
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return nil, s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get bucket policy: %w", err)
|
|
}
|
|
|
|
return policy, nil
|
|
}
|
|
|
|
func (p *Posix) DeleteBucketPolicy(ctx context.Context, bucket string) error {
|
|
if !p.isBucketValid(bucket) {
|
|
return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
return p.PutBucketPolicy(ctx, bucket, nil)
|
|
}
|
|
|
|
func (p *Posix) PutBucketCors(ctx context.Context, bucket string, cors []byte) error {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer release()
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
err = p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if cors == nil {
|
|
err = p.meta.DeleteAttribute(bucket, "", corskey)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return fmt.Errorf("remove cors: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
err = p.meta.StoreAttribute(nil, bucket, "", corskey, cors)
|
|
if err != nil {
|
|
return fmt.Errorf("set cors: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Posix) GetBucketCors(ctx context.Context, bucket string) ([]byte, error) {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer release()
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return nil, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
err = p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cors, err := p.meta.RetrieveAttribute(nil, bucket, "", corskey)
|
|
if errors.Is(err, meta.ErrNoSuchKey) {
|
|
return nil, s3err.GetBucketErr(s3err.ErrNoSuchCORSConfiguration, bucket)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return cors, nil
|
|
}
|
|
|
|
func (p *Posix) DeleteBucketCors(ctx context.Context, bucket string) error {
|
|
if !p.isBucketValid(bucket) {
|
|
return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
return p.PutBucketCors(ctx, bucket, nil)
|
|
}
|
|
|
|
func (p *Posix) PutBucketWebsite(ctx context.Context, bucket string, website []byte) error {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer release()
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return s3err.GetAPIError(s3err.ErrInvalidBucketName)
|
|
}
|
|
err = p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if website == nil {
|
|
err = p.meta.DeleteAttribute(bucket, "", websitekey)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return fmt.Errorf("remove website: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// The website configuration can be up to 128KB
|
|
// compress the data to fit in 64KB xattr limits
|
|
encoded, err := backend.MarshalWebsiteConfig(website, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = p.meta.StoreAttribute(nil, bucket, "", websitekey, encoded)
|
|
if err != nil {
|
|
return fmt.Errorf("set website: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Posix) GetBucketWebsite(ctx context.Context, bucket string) ([]byte, error) {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer release()
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return nil, s3err.GetAPIError(s3err.ErrInvalidBucketName)
|
|
}
|
|
err = p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
website, err := p.meta.RetrieveAttribute(nil, bucket, "", websitekey)
|
|
if errors.Is(err, meta.ErrNoSuchKey) {
|
|
return nil, s3err.GetBucketErr(s3err.ErrNoSuchWebsiteConfiguration, bucket)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
decoded, err := backend.UnmarshalWebsiteConfig(website, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return decoded, nil
|
|
}
|
|
|
|
func (p *Posix) DeleteBucketWebsite(ctx context.Context, bucket string) error {
|
|
if !p.isBucketValid(bucket) {
|
|
return s3err.GetAPIError(s3err.ErrInvalidBucketName)
|
|
}
|
|
return p.PutBucketWebsite(ctx, bucket, nil)
|
|
}
|
|
|
|
func (p *Posix) isBucketObjectLockEnabled(bucket string) error {
|
|
cfg, err := p.meta.RetrieveAttribute(nil, bucket, "", bucketLockKey)
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return s3err.GetBucketErr(s3err.ErrNoSuchBucket, bucket)
|
|
}
|
|
if errors.Is(err, meta.ErrNoSuchKey) {
|
|
return s3err.GetAPIError(s3err.ErrMissingObjectLockConfiguration)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("get object lock config: %w", err)
|
|
}
|
|
|
|
var bucketLockConfig auth.BucketLockConfig
|
|
if err := json.Unmarshal(cfg, &bucketLockConfig); err != nil {
|
|
return fmt.Errorf("parse bucket lock config: %w", err)
|
|
}
|
|
|
|
if !bucketLockConfig.Enabled {
|
|
return s3err.GetAPIError(s3err.ErrMissingObjectLockConfiguration)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Posix) PutObjectLockConfiguration(ctx context.Context, bucket string, config []byte) error {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer release()
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
err = p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if p.versioningEnabled() {
|
|
// if versioning is enabled on gateway level and bucket versioning
|
|
// status is not `Enabled`, object lock can't be enabled.
|
|
// if object lock has been enabled on bucket creation
|
|
// it means the versioning has been enabled alongside with object lock
|
|
// and it can't be suspended ever again
|
|
status, err := p.getBucketVersioningStatus(ctx, bucket)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if status != types.BucketVersioningStatusEnabled {
|
|
// if versioning is enabled on gateway level
|
|
return s3err.GetAPIError(s3err.ErrObjectLockConfigurationNotAllowed)
|
|
}
|
|
}
|
|
|
|
err = p.meta.StoreAttribute(nil, bucket, "", bucketLockKey, config)
|
|
if err != nil {
|
|
return fmt.Errorf("set object lock config: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Posix) GetObjectLockConfiguration(ctx context.Context, bucket string) ([]byte, error) {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer release()
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return nil, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
err = p.doesBucketExist(bucket)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cfg, err := p.meta.RetrieveAttribute(nil, bucket, "", bucketLockKey)
|
|
if errors.Is(err, meta.ErrNoSuchKey) {
|
|
return nil, s3err.GetBucketErr(s3err.ErrObjectLockConfigurationNotFound, bucket)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get object lock config: %w", err)
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
func (p *Posix) PutObjectLegalHold(ctx context.Context, bucket, object, versionId string, status bool) error {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer release()
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
err = p.doesBucketAndObjectExist(bucket, object, versionId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = p.isBucketObjectLockEnabled(bucket)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := p.validateVersionId(versionId); err != nil {
|
|
return err
|
|
}
|
|
|
|
var statusData []byte
|
|
if status {
|
|
statusData = []byte{1}
|
|
} else {
|
|
statusData = []byte{0}
|
|
}
|
|
|
|
if versionId != "" {
|
|
if !p.versioningEnabled() {
|
|
//TODO: Maybe we need to return our custom error here?
|
|
return s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, versionId)
|
|
}
|
|
bucket, object, err = p.objVersionAttrPath(bucket, object, versionId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
err = p.ensureNotDeleteMarker(bucket, object, versionId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = p.meta.StoreAttribute(nil, bucket, object, objectLegalHoldKey, statusData)
|
|
if errors.Is(err, fs.ErrNotExist) || isErrNotDir(err) {
|
|
if versionId != "" {
|
|
return s3err.GetNoSuchVersionErr(object, versionId)
|
|
}
|
|
return s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("set object lock config: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Posix) GetObjectLegalHold(ctx context.Context, bucket, object, versionId string) (*bool, error) {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer release()
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return nil, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
err = p.doesBucketAndObjectExist(bucket, object, versionId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = p.isBucketObjectLockEnabled(bucket)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := p.validateVersionId(versionId); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if versionId != "" {
|
|
if !p.versioningEnabled() {
|
|
//TODO: Maybe we need to return our custom error here?
|
|
return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, versionId)
|
|
}
|
|
bucket, object, err = p.objVersionAttrPath(bucket, object, versionId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
err = p.ensureNotDeleteMarker(bucket, object, versionId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
data, err := p.meta.RetrieveAttribute(nil, bucket, object, objectLegalHoldKey)
|
|
if errors.Is(err, fs.ErrNotExist) || isErrNotDir(err) {
|
|
if versionId != "" {
|
|
return nil, s3err.GetNoSuchVersionErr(object, versionId)
|
|
}
|
|
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
if errors.Is(err, meta.ErrNoSuchKey) {
|
|
return nil, s3err.GetAPIError(s3err.ErrNoSuchObjectLockConfiguration)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get object lock config: %w", err)
|
|
}
|
|
|
|
result := data[0] == 1
|
|
|
|
return &result, nil
|
|
}
|
|
|
|
func (p *Posix) PutObjectRetention(ctx context.Context, bucket, object, versionId string, retention []byte) error {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer release()
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
err = p.doesBucketAndObjectExist(bucket, object, versionId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = p.isBucketObjectLockEnabled(bucket)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := p.validateVersionId(versionId); err != nil {
|
|
return err
|
|
}
|
|
|
|
if versionId != "" {
|
|
if !p.versioningEnabled() {
|
|
//TODO: Maybe we need to return our custom error here?
|
|
return s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, versionId)
|
|
}
|
|
bucket, object, err = p.objVersionAttrPath(bucket, object, versionId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
err = p.ensureNotDeleteMarker(bucket, object, versionId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = p.meta.StoreAttribute(nil, bucket, object, objectRetentionKey, retention)
|
|
if err != nil {
|
|
return fmt.Errorf("set object lock config: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Posix) GetObjectRetention(ctx context.Context, bucket, object, versionId string) ([]byte, error) {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer release()
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return nil, s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
err = p.doesBucketAndObjectExist(bucket, object, versionId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = p.isBucketObjectLockEnabled(bucket)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := p.validateVersionId(versionId); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if versionId != "" {
|
|
if !p.versioningEnabled() {
|
|
//TODO: Maybe we need to return our custom error here?
|
|
return nil, s3err.GetInvalidArgumentErr(s3err.InvalidArgVersionId, versionId)
|
|
}
|
|
bucket, object, err = p.objVersionAttrPath(bucket, object, versionId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
err = p.ensureNotDeleteMarker(bucket, object, versionId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
data, err := p.meta.RetrieveAttribute(nil, bucket, object, objectRetentionKey)
|
|
if errors.Is(err, fs.ErrNotExist) || isErrNotDir(err) {
|
|
if versionId != "" {
|
|
return nil, s3err.GetNoSuchVersionErr(object, versionId)
|
|
}
|
|
return nil, s3err.GetAPIError(s3err.ErrNoSuchKey)
|
|
}
|
|
if errors.Is(err, meta.ErrNoSuchKey) {
|
|
return nil, s3err.GetAPIError(s3err.ErrNoSuchObjectLockConfiguration)
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get object lock config: %w", err)
|
|
}
|
|
|
|
return data, nil
|
|
}
|
|
|
|
func (p *Posix) ChangeBucketOwner(ctx context.Context, bucket, owner string) error {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer release()
|
|
|
|
if !p.isBucketValid(bucket) {
|
|
return s3err.GetBucketErr(s3err.ErrInvalidBucketName, bucket)
|
|
}
|
|
return auth.UpdateBucketACLOwner(ctx, p, bucket, owner)
|
|
}
|
|
|
|
// listBucketFileInfos returns the file info of every bucket directory under
|
|
// rootdir. With bucketlinks, a symlink to a directory counts as a bucket too.
|
|
func listBucketFileInfos(rootdir string, bucketlinks bool) ([]fs.FileInfo, error) {
|
|
entries, err := os.ReadDir(rootdir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("readdir buckets: %w", err)
|
|
}
|
|
|
|
var fis []fs.FileInfo
|
|
for _, entry := range entries {
|
|
if entry.Name() == objLockDir {
|
|
continue
|
|
}
|
|
|
|
fi, err := entry.Info()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
if bucketlinks && entry.Type() == fs.ModeSymlink {
|
|
fi, err = os.Stat(filepath.Join(rootdir, entry.Name()))
|
|
if err != nil {
|
|
// skip entries returning errors
|
|
continue
|
|
}
|
|
}
|
|
|
|
if !fi.IsDir() {
|
|
// buckets must be a directory
|
|
continue
|
|
}
|
|
|
|
fis = append(fis, fi)
|
|
}
|
|
|
|
return fis, nil
|
|
}
|
|
|
|
func (p *Posix) ListBucketsAndOwners(ctx context.Context) (buckets []s3response.Bucket, err error) {
|
|
release, err := p.acquireActionSlot(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer release()
|
|
|
|
fis, err := listBucketFileInfos(p.rootPath(), p.bucketlinks)
|
|
if err != nil {
|
|
return buckets, fmt.Errorf("listBucketFileInfos: %w", err)
|
|
}
|
|
|
|
for _, fi := range fis {
|
|
aclJSON, err := p.meta.RetrieveAttribute(nil, fi.Name(), "", aclkey)
|
|
if err != nil && !errors.Is(err, meta.ErrNoSuchKey) {
|
|
return buckets, fmt.Errorf("get acl tag: %w", err)
|
|
}
|
|
|
|
acl, err := auth.ParseACL(aclJSON)
|
|
if err != nil {
|
|
return buckets, fmt.Errorf("parse acl tag: %w", err)
|
|
}
|
|
|
|
buckets = append(buckets, s3response.Bucket{
|
|
Name: fi.Name(),
|
|
Owner: acl.Owner,
|
|
})
|
|
}
|
|
|
|
sort.SliceStable(buckets, func(i, j int) bool {
|
|
return buckets[i].Name < buckets[j].Name
|
|
})
|
|
|
|
return buckets, nil
|
|
}
|
|
|
|
// NormalizeObjectKey resolves object relative to bucket the same way the
|
|
// filesystem will (collapsing ".."/"." segments, catching a traversal
|
|
// attempt that escapes bucket), but the result names an S3 key, not a host
|
|
// path: on Windows filepath.Join/Rel would return it with "\" separators,
|
|
// which callers building a policy-resource ARN or match string must never
|
|
// see, so it's converted back to "/" before returning.
|
|
func (p *Posix) NormalizeObjectKey(bucket, object string) string {
|
|
fullPath := filepath.Join(bucket, object)
|
|
key, err := filepath.Rel(filepath.Clean(bucket), fullPath)
|
|
if err != nil {
|
|
return filepath.ToSlash(fullPath)
|
|
}
|
|
if key == "." {
|
|
return ""
|
|
}
|
|
|
|
return filepath.ToSlash(key)
|
|
}
|
|
|
|
func (p *Posix) storeChecksums(f *os.File, bucket, object string, chs s3response.Checksum) error {
|
|
checksums, err := json.Marshal(chs)
|
|
if err != nil {
|
|
return fmt.Errorf("parse checksum: %w", err)
|
|
}
|
|
|
|
return p.meta.StoreAttribute(f, bucket, object, checksumsKey, checksums)
|
|
}
|
|
|
|
func (p *Posix) retrieveChecksums(f *os.File, bucket, object string) (checksums s3response.Checksum, err error) {
|
|
checksumsAtr, err := p.meta.RetrieveAttribute(f, bucket, object, checksumsKey)
|
|
if err != nil {
|
|
return checksums, err
|
|
}
|
|
|
|
err = json.Unmarshal(checksumsAtr, &checksums)
|
|
return checksums, err
|
|
}
|
|
|
|
func getString(str *string) string {
|
|
if str == nil {
|
|
return ""
|
|
}
|
|
return *str
|
|
}
|
|
|
|
// drainBody consumes and discards all remaining bytes from r.
|
|
// It is called after a server error is detected mid-stream so that
|
|
// the client can read the error response before the write-side of the
|
|
// connection is shut down.
|
|
func drainBody(r io.Reader) {
|
|
if r == nil {
|
|
return
|
|
}
|
|
_, _ = io.Copy(io.Discard, r)
|
|
}
|
|
|
|
func joinPathWithTrailer(paths ...string) string {
|
|
joined := filepath.Join(paths...)
|
|
if strings.HasSuffix(paths[len(paths)-1], "/") {
|
|
joined += "/"
|
|
}
|
|
return joined
|
|
}
|