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 }