package s3api
import (
"errors"
"net/http"
"strings"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
"github.com/seaweedfs/seaweedfs/weed/util"
)
// An explicit directory marker - the key "dir/" created by PutObject on a
// trailing-slash key - is stored as the filer directory itself rather than as an
// object beside it. That makes it a poor fit for versioning: a delete marker would
// have to replace an entry that other keys live under, and the history would have to
// sit inside the directory it describes, where a listing keeps meeting it.
//
// So the key is not versioned. Deleting it does what deleting it in an unversioned
// bucket already does: the directory is removed when nothing is left under it, and
// demoted to a plain directory when children remain. Listings need no version lookup
// to tell what a directory stands for, and a bucket made of directory markers costs
// the same to list versioned as unversioned.
// deleteDirectoryMarker removes the key "
/". Callers hold the object write lock,
// so the entry this decides about cannot change between the read and the delete.
func (s3a *S3ApiServer) deleteDirectoryMarker(r *http.Request, bucket, object string) s3err.ErrorCode {
governanceBypassAllowed := s3a.evaluateGovernanceBypassRequest(r, bucket, object)
markerDir := s3a.bucketDir(bucket) + "/" + strings.TrimSuffix(strings.TrimPrefix(object, "/"), "/")
dir, name := util.FullPath(markerDir).DirAndName()
entry, err := s3a.getEntry(dir, name)
switch {
case errors.Is(err, filer_pb.ErrNotFound):
return s3err.ErrNone // deleting a key that is not there is a success
case err != nil:
// The entry may be a file a child write promoted to a directory, whose data
// belongs to the key without the trailing slash. Deleting without knowing
// would destroy it, so fail and leave the retry to the client.
glog.Errorf("deleteDirectoryMarker: cannot read %s/%s: %v", bucket, object, err)
return s3err.ErrInternalError
case len(entry.GetChunks()) > 0 || entry.IsInRemoteOnly():
// A promoted file, not a marker: "dir/" does not name its data.
glog.V(2).Infof("deleteDirectoryMarker: %s/%s holds uploaded data, leaving it alone", bucket, object)
return s3err.ErrNone
}
// The key is deleted the unversioned way, but Object Lock still covers it: the
// gateway lists it as an object and serves retention set on it. The lock that
// matters is the one on this entry, since that is what is removed -- looking the
// key up instead would answer with a version once the key has a history.
if err := s3a.enforceObjectLockOnEntry(entry, bucket, object, "", governanceBypassAllowed); err != nil {
glog.V(2).Infof("deleteDirectoryMarker: %s/%s is locked: %v", bucket, object, err)
return s3err.ErrAccessDenied
}
// Drop a history an older build recorded for this key. Nothing writes one now, and
// leaving it behind keeps reporting the key in ListObjectVersions, so a history we
// cannot read or remove fails the delete rather than half finishing it.
switch _, historyErr := s3a.getEntry(markerDir, s3_constants.VersionsFolder); {
case historyErr == nil:
// The removal below takes every entry under the key, so each has to be clear
// of a lock of its own.
versionsDir := markerDir + "/" + s3_constants.VersionsFolder
for startFrom := ""; ; {
entries, isLast, listErr := s3a.list(versionsDir, "", startFrom, false, 1000)
if listErr != nil {
glog.Errorf("deleteDirectoryMarker: cannot list history of %s/%s: %v", bucket, object, listErr)
return s3err.ErrInternalError
}
for _, entry := range entries {
startFrom = entry.Name
versionId, named := entry.Extended[s3_constants.ExtVersionIdKey]
if !named {
// An entry an older build left without a version id is what this
// removal is here to clear, but one still under a lock cannot be
// named to check it, so judge it on what it carries itself.
if err := s3a.enforceObjectLockOnEntry(entry, bucket, object, "", governanceBypassAllowed); err != nil {
glog.V(2).Infof("deleteDirectoryMarker: unnamed history entry %s of %s/%s is locked: %v", entry.Name, bucket, object, err)
return s3err.ErrAccessDenied
}
continue
}
if err := s3a.enforceObjectLockProtections(r, bucket, object, string(versionId), governanceBypassAllowed); err != nil {
glog.V(2).Infof("deleteDirectoryMarker: version %s of %s/%s is locked: %v", versionId, bucket, object, err)
return s3err.ErrAccessDenied
}
}
if isLast || len(entries) == 0 {
break
}
}
if rmErr := s3a.rm(r.Context(), markerDir, s3_constants.VersionsFolder, true, true); rmErr != nil {
glog.Errorf("deleteDirectoryMarker: failed to remove stale history of %s/%s: %v", bucket, object, rmErr)
return s3err.ErrInternalError
}
case !errors.Is(historyErr, filer_pb.ErrNotFound):
glog.Errorf("deleteDirectoryMarker: cannot read stale history of %s/%s: %v", bucket, object, historyErr)
return s3err.ErrInternalError
}
if err := s3a.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
return s3a.deleteUnversionedObjectWithClient(r.Context(), client, bucket, object, false)
}); err != nil {
glog.Errorf("deleteDirectoryMarker: failed to delete %s/%s: %v", bucket, object, err)
return s3err.ErrInternalError
}
return s3err.ErrNone
}