Files
versitygw/backend/posix/posix.go
T
niksis02 cdbebb06d6 fix: place the null version by its recorded position in posix versioning
`latestObjVersion` and `fileToObjVersions` placed the null version among the `ulid` versions by comparing file modification times, but a delete marker keeps the data and mtime of the version it hides. A null delete marker created in a versioning-suspended bucket therefore tied with the version below it and lost. Deleting the current version of an object whose history held a null delete marker restored the older version instead of the delete marker, and `ListObjectVersions` listed the delete marker below that version. Directory objects were exposed too, as a directory's mtime changes when objects are created under it.

`createObjVersion` now records the position of the null version when it moves it to the versioning directory. At that moment the null version is the current one, so it is newer than every version already there and older than every version created later. The id of the newest version in the directory is stored in the new `null-version-prev` attribute, and `nullVersionPos` compares it with the `ulid` version ids for both `DeleteObject` and `ListObjectVersions`. Null versions moved before this change carry no attribute and still fall back to the modification time. The attribute is not copied onto the object when a null version is restored.

`fileToObjVersions` also listed the null version without checking the `VersionIdMarker`, so it was repeated on every later page, and a `null` marker never matched, which dropped every version below it. The current version was listed again when it was the marker, so paging with the marker at a current version looped forever. The null version is now listed through `addNullVersion`, which skips it before the marker and continues after it when it is the marker, and the current version is no longer listed again when the marker names it.
2026-09-24 16:58:30 +04:00

7876 lines
241 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"
"slices"
"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"
nullVersionPrevKey = "null-version-prev"
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
}
// liveObjVersionId returns the version id to report for the current version
// of bucket/object. An object with no versionId attribute is the null
// version, but only a bucket with versioning configured has versions at all:
// in a bucket that was never versioned the object has no version id.
func (p *Posix) liveObjVersionId(ctx context.Context, bucket, object string) (string, error) {
vId, err := p.meta.RetrieveAttribute(nil, bucket, object, versionIdKey)
if err == nil {
return string(vId), nil
}
if !errors.Is(err, meta.ErrNoSuchKey) {
return "", fmt.Errorf("get obj versionId: %w", err)
}
status, err := p.getBucketVersioningStatus(ctx, bucket)
if err != nil {
return "", fmt.Errorf("get bucket versioning status: %w", err)
}
if status == "" {
return "", nil
}
return nullVersionId, nil
}
// 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 versionId == nullVersionId {
// the null version id doesn't sort with the ulid version ids, so
// record its place in the version history: it's the current
// version, which makes it newer than the versions already in the
// versioning directory and older than any version created later
prev, err := newestObjVersionId(filepath.Join(versionBucketPath, genObjVersionKey(key)))
if err != nil {
return versionPath, fmt.Errorf("get newest object version: %w", err)
}
err = p.meta.StoreAttribute(f.File(), versionPath, "", nullVersionPrevKey, []byte(prev))
if err != nil {
return versionPath, fmt.Errorf("store %v attribute: %w", nullVersionPrevKey, 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)
}
// the version id marker is the version listed last, so the
// listing continues with the version following it
if !*pastVersionIdMarker {
*pastVersionIdMarker = versionId == versionIdMarker
} else {
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
var nullPos nullVersionPos
nf, err := os.Stat(filepath.Join(versionPath, nullVersionId))
if err != nil && !errors.Is(err, fs.ErrNotExist) {
return nil, err
}
if err == nil {
nullPos, err = p.getNullVersionPos(versionPath, nf)
if err != nil {
return nil, err
}
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
isNullVersionIdObjAdded := false
// addNullVersion lists the null version at its place in the
// version history, or continues the listing after it when it's
// the version id marker. It returns the truncated result when
// the null version fills up the listing.
addNullVersion := func() *backend.ObjVersionFuncResult {
isNullVersionIdObjAdded = true
if !*pastVersionIdMarker {
*pastVersionIdMarker = versionIdMarker == nullVersionId
return nil
}
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,
}
}
return nil
}
for _, dEntry := range slices.Backward(dirEnts) {
// 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 place in the version history, then
// continue the adding
if isNullVersionIdObjFound && !isNullVersionIdObjAdded && nullPos.isNewerThan(f) {
if res := addNullVersion(); res != nil {
return res, 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 res := addNullVersion(); res != nil {
return res, 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 := p.latestObjVersion(versionPath, ents)
if err != nil {
return nil, fmt.Errorf("get latest obj version: %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 {
// the place of the null version only applies in
// the versioning directory
if attr == nullVersionPrevKey {
continue
}
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
}
}
// a bucket that has never been versioned only holds null versions, so
// any other versionId names a version that doesn't exist: AWS returns
// success and deletes nothing
if versionId := getString(input.VersionId); versionId != "" && versionId != nullVersionId {
return &s3.DeleteObjectOutput{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
}
// newestObjVersionId returns the id of the newest version, other than the
// null version, in the object versioning directory at versionPath, or an
// empty string if it has none. The entries are named after the version id
// and os.ReadDir sorts them by name, which puts the ulid version ids in
// creation order.
func newestObjVersionId(versionPath string) (string, error) {
ents, err := os.ReadDir(versionPath)
if errors.Is(err, fs.ErrNotExist) {
return "", nil
}
if err != nil {
return "", err
}
for _, ent := range slices.Backward(ents) {
if ent.Name() != nullVersionId {
return ent.Name(), nil
}
}
return "", nil
}
// nullVersionPos is the place of the null version in the version history of
// an object, among the versions ordered by their ulid version ids
type nullVersionPos struct {
// prev is the id of the newest version older than the null version,
// empty if the null version is the oldest one
prev string
// modTime places a null version that was moved to the versioning
// directory before its place was recorded
modTime *time.Time
}
// isNewerThan reports whether the null version is newer than the version
// fi, an entry of the object versioning directory named after its version id
func (n nullVersionPos) isNewerThan(fi fs.FileInfo) bool {
if n.modTime != nil {
return n.modTime.After(fi.ModTime())
}
return fi.Name() <= n.prev
}
// getNullVersionPos returns the place of the null version, nullInfo, in the
// object versioning directory at versionPath
func (p *Posix) getNullVersionPos(versionPath string, nullInfo fs.FileInfo) (nullVersionPos, error) {
prev, err := p.meta.RetrieveAttribute(nil, versionPath, nullVersionId, nullVersionPrevKey)
if errors.Is(err, meta.ErrNoSuchKey) {
modTime := nullInfo.ModTime()
return nullVersionPos{modTime: &modTime}, nil
}
if err != nil {
return nullVersionPos{}, fmt.Errorf("get %v attribute: %w", nullVersionPrevKey, err)
}
return nullVersionPos{prev: string(prev)}, nil
}
// latestObjVersion returns the version, among the entries of the object
// versioning directory at versionPath, that becomes the latest one when the
// latest version of the object is deleted
func (p *Posix) latestObjVersion(versionPath string, ents []fs.DirEntry) (fs.FileInfo, error) {
var latest, nullEnt fs.DirEntry
for _, ent := range ents {
if ent.Name() == nullVersionId {
nullEnt = ent
continue
}
latest = ent
}
switch {
case latest == nil && nullEnt == nil:
return nil, fs.ErrNotExist
case latest == nil:
return nullEnt.Info()
case nullEnt == nil:
return latest.Info()
}
latestInfo, err := latest.Info()
if err != nil {
return nil, err
}
nullInfo, err := nullEnt.Info()
if err != nil {
return nil, err
}
pos, err := p.getNullVersionPos(versionPath, nullInfo)
if err != nil {
return nil, err
}
if pos.isNewerThan(latestInfo) {
return nullInfo, nil
}
return latestInfo, nil
}
// 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
}
srcInfo, err := p.latestObjVersion(versionPath, ents)
if err != nil {
return fmt.Errorf("get latest obj version: %w", err)
}
srcVersionId := srcInfo.Name()
// 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 == "" {
versionId, err = p.liveObjVersionId(ctx, bucket, object)
if err != nil {
return nil, err
}
}
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) {
// an object without a versionId attribute is the null version
vId = []byte(nullVersionId)
}
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 == "" {
versionId, err = p.liveObjVersionId(ctx, bucket, object)
if err != nil {
return nil, err
}
}
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
}