Repository: googleapis/google-cloud-go (Go)

Issue:
storage: implement soft delete
Please implement the soft delete feature as specified in go/gcs-soft-delete-client-request

The operations we need are
- Set a soft delete policy on a new or existing bucket
- Get a bucket's soft delete policy
- Get a soft deleted object by doing an object get call with the soft deleted parameter set to true
- Get the soft delete time and hard delete time metadata fields from a soft deleted object
- List soft deleted objects by doing an object list call with the soft deleted parameter set to true
- Restore a soft deleted object

You can use the [Java implementation](https://github.com/googleapis/java-storage/pull/2403) as a reference




Fix (patch):
diff --git a/storage/bucket.go b/storage/bucket.go
index 0344ef9de310..d2da86e914b3 100644
--- a/storage/bucket.go
+++ b/storage/bucket.go
@@ -479,6 +479,13 @@ type BucketAttrs struct {
 	// cannot be modified once the bucket is created.
 	// ObjectRetention cannot be configured or reported through the gRPC API.
 	ObjectRetentionMode string
+
+	// SoftDeletePolicy contains the bucket's soft delete policy, which defines
+	// the period of time that soft-deleted objects will be retained, and cannot
+	// be permanently deleted. By default, new buckets will be created with a
+	// 7 day retention duration. In order to fully disable soft delete, you need
+	// to set a policy with a RetentionDuration of 0.
+	SoftDeletePolicy *SoftDeletePolicy
 }
 
 // BucketPolicyOnly is an alias for UniformBucketLevelAccess.
@@ -766,6 +773,19 @@ type Autoclass struct {
 	TerminalStorageClassUpdateTime time.Time
 }
 
+// SoftDeletePolicy contains the bucket's soft delete policy, which defines the
+// period of time that soft-deleted objects will be retained, and cannot be
+// permanently deleted.
+type SoftDeletePolicy struct {
+	// EffectiveTime indicates the time from which the policy, or one with a
+	// greater retention, was effective. This field is read-only.
+	EffectiveTime time.Time
+
+	// RetentionDuration is the amount of time that soft-deleted objects in the
+	// bucket will be retained and cannot be permanently deleted.
+	RetentionDuration time.Duration
+}
+
 func newBucket(b *raw.Bucket) (*BucketAttrs, error) {
 	if b == nil {
 		return nil, nil
@@ -803,6 +823,7 @@ func newBucket(b *raw.Bucket) (*BucketAttrs, error) {
 		RPO:                      toRPO(b),
 		CustomPlacementConfig:    customPlacementFromRaw(b.CustomPlacementConfig),
 		Autoclass:                toAutoclassFromRaw(b.Autoclass),
+		SoftDeletePolicy:         toSoftDeletePolicyFromRaw(b.SoftDeletePolicy),
 	}, nil
 }
 
@@ -836,6 +857,7 @@ func newBucketFromProto(b *storagepb.Bucket) *BucketAttrs {
 		CustomPlacementConfig:    customPlacementFromProto(b.GetCustomPlacementConfig()),
 		ProjectNumber:            parseProjectNumber(b.GetProject()), // this can return 0 the project resource name is ID based
 		Autoclass:                toAutoclassFromProto(b.GetAutoclass()),
+		SoftDeletePolicy:         toSoftDeletePolicyFromProto(b.SoftDeletePolicy),
 	}
 }
 
@@ -891,6 +913,7 @@ func (b *BucketAttrs) toRawBucket() *raw.Bucket {
 		Rpo:                   b.RPO.String(),
 		CustomPlacementConfig: b.CustomPlacementConfig.toRawCustomPlacement(),
 		Autoclass:             b.Autoclass.toRawAutoclass(),
+		SoftDeletePolicy:      b.SoftDeletePolicy.toRawSoftDeletePolicy(),
 	}
 }
 
@@ -951,6 +974,7 @@ func (b *BucketAttrs) toProtoBucket() *storagepb.Bucket {
 		Rpo:                   b.RPO.String(),
 		CustomPlacementConfig: b.CustomPlacementConfig.toProtoCustomPlacement(),
 		Autoclass:             b.Autoclass.toProtoAutoclass(),
+		SoftDeletePolicy:      b.SoftDeletePolicy.toProtoSoftDeletePolicy(),
 	}
 }
 
@@ -1032,6 +1056,7 @@ func (ua *BucketAttrsToUpdate) toProtoBucket() *storagepb.Bucket {
 		IamConfig:             bktIAM,
 		Rpo:                   ua.RPO.String(),
 		Autoclass:             ua.Autoclass.toProtoAutoclass(),
+		SoftDeletePolicy:      ua.SoftDeletePolicy.toProtoSoftDeletePolicy(),
 		Labels:                ua.setLabels,
 	}
 }
@@ -1152,6 +1177,9 @@ type BucketAttrsToUpdate struct {
 	// See https://cloud.google.com/storage/docs/using-autoclass for more information.
 	Autoclass *Autoclass
 
+	// If set, updates the soft delete policy of the bucket.
+	SoftDeletePolicy *SoftDeletePolicy
+
 	// acl is the list of access control rules on the bucket.
 	// It is unexported and only used internally by the gRPC client.
 	// Library users should use ACLHandle methods directly.
@@ -1273,6 +1301,14 @@ func (ua *BucketAttrsToUpdate) toRawBucket() *raw.Bucket {
 		}
 		rb.ForceSendFields = append(rb.ForceSendFields, "Autoclass")
 	}
+	if ua.SoftDeletePolicy != nil {
+		if ua.SoftDeletePolicy.RetentionDuration == 0 {
+			rb.NullFields = append(rb.NullFields, "SoftDeletePolicy")
+			rb.SoftDeletePolicy = nil
+		} else {
+			rb.SoftDeletePolicy = ua.SoftDeletePolicy.toRawSoftDeletePolicy()
+		}
+	}
 	if ua.PredefinedACL != "" {
 		// Clear ACL or the call will fail.
 		rb.Acl = nil
@@ -2053,6 +2089,53 @@ func toAutoclassFromProto(a *storagepb.Bucket_Autoclass) *Autoclass {
 	}
 }
 
+func (p *SoftDeletePolicy) toRawSoftDeletePolicy() *raw.BucketSoftDeletePolicy {
+	if p == nil {
+		return nil
+	}
+	// Excluding read only field EffectiveTime.
+	return &raw.BucketSoftDeletePolicy{
+		RetentionDurationSeconds: int64(p.RetentionDuration.Seconds()),
+	}
+}
+
+func (p *SoftDeletePolicy) toProtoSoftDeletePolicy() *storagepb.Bucket_SoftDeletePolicy {
+	if p == nil {
+		return nil
+	}
+	// Excluding read only field EffectiveTime.
+	return &storagepb.Bucket_SoftDeletePolicy{
+		RetentionDuration: durationpb.New(p.RetentionDuration),
+	}
+}
+
+func toSoftDeletePolicyFromRaw(p *raw.BucketSoftDeletePolicy) *SoftDeletePolicy {
+	if p == nil {
+		return nil
+	}
+
+	policy := &SoftDeletePolicy{
+		RetentionDuration: time.Duration(p.RetentionDurationSeconds) * time.Second,
+	}
+
+	// Return EffectiveTime only if parsed to a valid value.
+	if t, err := time.Parse(time.RFC3339, p.EffectiveTime); err == nil {
+		policy.EffectiveTime = t
+	}
+
+	return policy
+}
+
+func toSoftDeletePolicyFromProto(p *storagepb.Bucket_SoftDeletePolicy) *SoftDeletePolicy {
+	if p == nil {
+		return nil
+	}
+	return &SoftDeletePolicy{
+		EffectiveTime:     p.GetEffectiveTime().AsTime(),
+		RetentionDuration: p.GetRetentionDuration().AsDuration(),
+	}
+}
+
 // Objects returns an iterator over the objects in the bucket that match the
 // Query q. If q is nil, no filtering is done. Objects will be iterated over
 // lexicographically by name.
diff --git a/storage/client.go b/storage/client.go
index 70b2a280e3b3..187c3ec75a02 100644
--- a/storage/client.go
+++ b/storage/client.go
@@ -59,8 +59,9 @@ type storageClient interface {
 	// Object metadata methods.
 
 	DeleteObject(ctx context.Context, bucket, object string, gen int64, conds *Conditions, opts ...storageOption) error
-	GetObject(ctx context.Context, bucket, object string, gen int64, encryptionKey []byte, conds *Conditions, opts ...storageOption) (*ObjectAttrs, error)
+	GetObject(ctx context.Context, params *getObjectParams, opts ...storageOption) (*ObjectAttrs, error)
 	UpdateObject(ctx context.Context, params *updateObjectParams, opts ...storageOption) (*ObjectAttrs, error)
+	RestoreObject(ctx context.Context, params *restoreObjectParams, opts ...storageOption) (*ObjectAttrs, error)
 
 	// Default Object ACL methods.
 
@@ -294,6 +295,14 @@ type newRangeReaderParams struct {
 	readCompressed bool // Use accept-encoding: gzip. Only works for HTTP currently.
 }
 
+type getObjectParams struct {
+	bucket, object string
+	gen            int64
+	encryptionKey  []byte
+	conds          *Conditions
+	softDeleted    bool
+}
+
 type updateObjectParams struct {
 	bucket, object    string
 	uattrs            *ObjectAttrsToUpdate
@@ -303,6 +312,14 @@ type updateObjectParams struct {
 	overrideRetention *bool
 }
 
+type restoreObjectParams struct {
+	bucket, object string
+	gen            int64
+	encryptionKey  []byte
+	conds          *Conditions
+	copySourceACL  bool
+}
+
 type composeObjectRequest struct {
 	dstBucket     string
 	dstObject     destinationObject
diff --git a/storage/grpc_client.go b/storage/grpc_client.go
index 1f34dff53029..c5e2e3983d94 100644
--- a/storage/grpc_client.go
+++ b/storage/grpc_client.go
@@ -367,6 +367,9 @@ func (c *grpcStorageClient) UpdateBucket(ctx context.Context, bucket string, uat
 	if uattrs.Autoclass != nil {
 		fieldMask.Paths = append(fieldMask.Paths, "autoclass")
 	}
+	if uattrs.SoftDeletePolicy != nil {
+		fieldMask.Paths = append(fieldMask.Paths, "soft_delete_policy")
+	}
 
 	for label := range uattrs.setLabels {
 		fieldMask.Paths = append(fieldMask.Paths, fmt.Sprintf("labels.%s", label))
@@ -421,6 +424,7 @@ func (c *grpcStorageClient) ListObjects(ctx context.Context, bucket string, q *Q
 		IncludeTrailingDelimiter: it.query.IncludeTrailingDelimiter,
 		MatchGlob:                it.query.MatchGlob,
 		ReadMask:                 q.toFieldMask(), // a nil Query still results in a "*" FieldMask
+		SoftDeleted:              it.query.SoftDeleted,
 	}
 	if s.userProject != "" {
 		ctx = setUserProjectMetadata(ctx, s.userProject)
@@ -490,22 +494,25 @@ func (c *grpcStorageClient) DeleteObject(ctx context.Context, bucket, object str
 	return err
 }
 
-func (c *grpcStorageClient) GetObject(ctx context.Context, bucket, object string, gen int64, encryptionKey []byte, conds *Conditions, opts ...storageOption) (*ObjectAttrs, error) {
+func (c *grpcStorageClient) GetObject(ctx context.Context, params *getObjectParams, opts ...storageOption) (*ObjectAttrs, error) {
 	s := callSettings(c.settings, opts...)
 	req := &storagepb.GetObjectRequest{
-		Bucket: bucketResourceName(globalProjectAlias, bucket),
-		Object: object,
+		Bucket: bucketResourceName(globalProjectAlias, params.bucket),
+		Object: params.object,
 		// ProjectionFull by default.
 		ReadMask: &fieldmaskpb.FieldMask{Paths: []string{"*"}},
 	}
-	if err := applyCondsProto("grpcStorageClient.GetObject", gen, conds, req); err != nil {
+	if err := applyCondsProto("grpcStorageClient.GetObject", params.gen, params.conds, req); err != nil {
 		return nil, err
 	}
 	if s.userProject != "" {
 		ctx = setUserProjectMetadata(ctx, s.userProject)
 	}
-	if encryptionKey != nil {
-		req.CommonObjectRequestParams = toProtoCommonObjectRequestParams(encryptionKey)
+	if params.encryptionKey != nil {
+		req.CommonObjectRequestParams = toProtoCommonObjectRequestParams(params.encryptionKey)
+	}
+	if params.softDeleted {
+		req.SoftDeleted = &params.softDeleted
 	}
 
 	var attrs *ObjectAttrs
@@ -608,6 +615,32 @@ func (c *grpcStorageClient) UpdateObject(ctx context.Context, params *updateObje
 	return attrs, err
 }
 
+func (c *grpcStorageClient) RestoreObject(ctx context.Context, params *restoreObjectParams, opts ...storageOption) (*ObjectAttrs, error) {
+	s := callSettings(c.settings, opts...)
+	req := &storagepb.RestoreObjectRequest{
+		Bucket:        bucketResourceName(globalProjectAlias, params.bucket),
+		Object:        params.object,
+		CopySourceAcl: &params.copySourceACL,
+	}
+	if err := applyCondsProto("grpcStorageClient.RestoreObject", params.gen, params.conds, req); err != nil {
+		return nil, err
+	}
+	if s.userProject != "" {
+		ctx = setUserProjectMetadata(ctx, s.userProject)
+	}
+
+	var attrs *ObjectAttrs
+	err := run(ctx, func(ctx context.Context) error {
+		res, err := c.raw.RestoreObject(ctx, req, s.gax...)
+		attrs = newObjectFromProto(res)
+		return err
+	}, s.retry, s.idempotent)
+	if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound {
+		return nil, ErrObjectNotExist
+	}
+	return attrs, err
+}
+
 // Default Object ACL methods.
 
 func (c *grpcStorageClient) DeleteDefaultObjectACL(ctx context.Context, bucket string, entity ACLEntity, opts ...storageOption) error {
@@ -737,7 +770,7 @@ func (c *grpcStorageClient) UpdateBucketACL(ctx context.Context, bucket string,
 func (c *grpcStorageClient) DeleteObjectACL(ctx context.Context, bucket, object string, entity ACLEntity, opts ...storageOption) error {
 	// There is no separate API for PATCH in gRPC.
 	// Make a GET call first to retrieve ObjectAttrs.
-	attrs, err := c.GetObject(ctx, bucket, object, defaultGen, nil, nil, opts...)
+	attrs, err := c.GetObject(ctx, &getObjectParams{bucket, object, defaultGen, nil, nil, false}, opts...)
 	if err != nil {
 		return err
 	}
@@ -770,7 +803,7 @@ func (c *grpcStorageClient) DeleteObjectACL(ctx context.Context, bucket, object
 // ListObjectACLs retrieves object ACL entries. By default, it operates on the latest generation of this object.
 // Selecting a specific generation of this object is not currently supported by the client.
 func (c *grpcStorageClient) ListObjectACLs(ctx context.Context, bucket, object string, opts ...storageOption) ([]ACLRule, error) {
-	o, err := c.GetObject(ctx, bucket, object, defaultGen, nil, nil, opts...)
+	o, err := c.GetObject(ctx, &getObjectParams{bucket, object, defaultGen, nil, nil, false}, opts...)
 	if err != nil {
 		return nil, err
 	}
@@ -780,7 +813,7 @@ func (c *grpcStorageClient) ListObjectACLs(ctx context.Context, bucket, object s
 func (c *grpcStorageClient) UpdateObjectACL(ctx context.Context, bucket, object string, entity ACLEntity, role ACLRole, opts ...storageOption) error {
 	// There is no separate API for PATCH in gRPC.
 	// Make a GET call first to retrieve ObjectAttrs.
-	attrs, err := c.GetObject(ctx, bucket, object, defaultGen, nil, nil, opts...)
+	attrs, err := c.GetObject(ctx, &getObjectParams{bucket, object, defaultGen, nil, nil, false}, opts...)
 	if err != nil {
 		return err
 	}
diff --git a/storage/http_client.go b/storage/http_client.go
index f75d93897d9d..4130a5e95213 100644
--- a/storage/http_client.go
+++ b/storage/http_client.go
@@ -337,6 +337,9 @@ func (c *httpStorageClient) ListObjects(ctx context.Context, bucket string, q *Q
 	}
 	fetch := func(pageSize int, pageToken string) (string, error) {
 		req := c.raw.Objects.List(bucket)
+		if it.query.SoftDeleted {
+			req.SoftDeleted(it.query.SoftDeleted)
+		}
 		setClientHeader(req.Header())
 		projection := it.query.Projection
 		if projection == ProjectionDefault {
@@ -409,18 +412,22 @@ func (c *httpStorageClient) DeleteObject(ctx context.Context, bucket, object str
 	return err
 }
 
-func (c *httpStorageClient) GetObject(ctx context.Context, bucket, object string, gen int64, encryptionKey []byte, conds *Conditions, opts ...storageOption) (*ObjectAttrs, error) {
+func (c *httpStorageClient) GetObject(ctx context.Context, params *getObjectParams, opts ...storageOption) (*ObjectAttrs, error) {
 	s := callSettings(c.settings, opts...)
-	req := c.raw.Objects.Get(bucket, object).Projection("full").Context(ctx)
-	if err := applyConds("Attrs", gen, conds, req); err != nil {
+	req := c.raw.Objects.Get(params.bucket, params.object).Projection("full").Context(ctx)
+	if err := applyConds("Attrs", params.gen, params.conds, req); err != nil {
 		return nil, err
 	}
 	if s.userProject != "" {
 		req.UserProject(s.userProject)
 	}
-	if err := setEncryptionHeaders(req.Header(), encryptionKey, false); err != nil {
+	if err := setEncryptionHeaders(req.Header(), params.encryptionKey, false); err != nil {
 		return nil, err
 	}
+	if params.softDeleted {
+		req.SoftDeleted(params.softDeleted)
+	}
+
 	var obj *raw.Object
 	var err error
 	err = run(ctx, func(ctx context.Context) error {
@@ -547,6 +554,33 @@ func (c *httpStorageClient) UpdateObject(ctx context.Context, params *updateObje
 	return newObject(obj), nil
 }
 
+func (c *httpStorageClient) RestoreObject(ctx context.Context, params *restoreObjectParams, opts ...storageOption) (*ObjectAttrs, error) {
+	s := callSettings(c.settings, opts...)
+	req := c.raw.Objects.Restore(params.bucket, params.object, params.gen).Context(ctx)
+	// Do not set the generation here since it's not an optional condition; it gets set above.
+	if err := applyConds("RestoreObject", defaultGen, params.conds, req); err != nil {
+		return nil, err
+	}
+	if s.userProject != "" {
+		req.UserProject(s.userProject)
+	}
+	if params.copySourceACL {
+		req.CopySourceAcl(params.copySourceACL)
+	}
+	if err := setEncryptionHeaders(req.Header(), params.encryptionKey, false); err != nil {
+		return nil, err
+	}
+
+	var obj *raw.Object
+	var err error
+	err = run(ctx, func(ctx context.Context) error { obj, err = req.Context(ctx).Do(); return err }, s.retry, s.idempotent)
+	var e *googleapi.Error
+	if ok := errors.As(err, &e); ok && e.Code == http.StatusNotFound {
+		return nil, ErrObjectNotExist
+	}
+	return newObject(obj), err
+}
+
 // Default Object ACL methods.
 
 func (c *httpStorageClient) DeleteDefaultObjectACL(ctx context.Context, bucket string, entity ACLEntity, opts ...storageOption) error {
diff --git a/storage/storage.go b/storage/storage.go
index c01085f35dda..f80dde98159a 100644
--- a/storage/storage.go
+++ b/storage/storage.go
@@ -898,6 +898,7 @@ type ObjectHandle struct {
 	readCompressed    bool   // Accept-Encoding: gzip
 	retry             *retryConfig
 	overrideRetention *bool
+	softDeleted       bool
 }
 
 // ACL provides access to the object's access control list.
@@ -952,7 +953,7 @@ func (o *ObjectHandle) Attrs(ctx context.Context) (attrs *ObjectAttrs, err error
 		return nil, err
 	}
 	opts := makeStorageOpts(true, o.retry, o.userProject)
-	return o.c.tc.GetObject(ctx, o.bucket, o.object, o.gen, o.encryptionKey, o.conds, opts...)
+	return o.c.tc.GetObject(ctx, &getObjectParams{o.bucket, o.object, o.gen, o.encryptionKey, o.conds, o.softDeleted}, opts...)
 }
 
 // Update updates an object with the provided attributes. See
@@ -1057,6 +1058,50 @@ func (o *ObjectHandle) OverrideUnlockedRetention(override bool) *ObjectHandle {
 	return &o2
 }
 
+// SoftDeleted returns an object handle that can be used to get an object that
+// has been soft deleted. To get a soft deleted object, the generation must be
+// set on the object using ObjectHandle.Generation.
+// Note that an error will be returned if a live object is queried using this.
+func (o *ObjectHandle) SoftDeleted() *ObjectHandle {
+	o2 := *o
+	o2.softDeleted = true
+	return &o2
+}
+
+// RestoreOptions allows you to set options when restoring an object.
+type RestoreOptions struct {
+	/// CopySourceACL indicates whether the restored object should copy the
+	// access controls of the source object. Only valid for buckets with
+	// fine-grained access. If uniform bucket-level access is enabled, setting
+	// CopySourceACL will cause an error.
+	CopySourceACL bool
+}
+
+// Restore will restore a soft-deleted object to a live object.
+// Note that you must specify a generation to use this method.
+func (o *ObjectHandle) Restore(ctx context.Context, opts *RestoreOptions) (*ObjectAttrs, error) {
+	if err := o.validate(); err != nil {
+		return nil, err
+	}
+
+	// Since the generation is required by restore calls, we set the default to
+	// 0 instead of a negative value, which returns a more descriptive error.
+	gen := o.gen
+	if o.gen == defaultGen {
+		gen = 0
+	}
+
+	// Restore is always idempotent because Generation is a required param.
+	sOpts := makeStorageOpts(true, o.retry, o.userProject)
+	return o.c.tc.RestoreObject(ctx, &restoreObjectParams{
+		bucket:        o.bucket,
+		object:        o.object,
+		gen:           gen,
+		conds:         o.conds,
+		copySourceACL: opts.CopySourceACL,
+	}, sOpts...)
+}
+
 // NewWriter returns a storage Writer that writes to the GCS object
 // associated with this ObjectHandle.
 //
@@ -1390,6 +1435,21 @@ type ObjectAttrs struct {
 	// Retention contains the retention configuration for this object.
 	// ObjectRetention cannot be configured or reported through the gRPC API.
 	Retention *ObjectRetention
+
+	// SoftDeleteTime is the time when the object became soft-deleted.
+	// Soft-deleted objects are only accessible on an object handle returned by
+	// ObjectHandle.SoftDeleted; if ObjectHandle.SoftDeleted has not been set,
+	// ObjectHandle.Attrs will return ErrObjectNotExist if the object is soft-deleted.
+	// This field is read-only.
+	SoftDeleteTime time.Time
+
+	// HardDeleteTime is the time when the object will be permanently deleted.
+	// Only set when an object becomes soft-deleted with a soft delete policy.
+	// Soft-deleted objects are only accessible on an object handle returned by
+	// ObjectHandle.SoftDeleted; if ObjectHandle.SoftDeleted has not been set,
+	// ObjectHandle.Attrs will return ErrObjectNotExist if the object is soft-deleted.
+	// This field is read-only.
+	HardDeleteTime time.Time
 }
 
 // ObjectRetention contains the retention configuration for this object.
@@ -1494,6 +1554,8 @@ func newObject(o *raw.Object) *ObjectAttrs {
 		CustomTime:              convertTime(o.CustomTime),
 		ComponentCount:          o.ComponentCount,
 		Retention:               toObjectRetention(o.Retention),
+		SoftDeleteTime:          convertTime(o.SoftDeleteTime),
+		HardDeleteTime:          convertTime(o.HardDeleteTime),
 	}
 }
 
@@ -1529,6 +1591,8 @@ func newObjectFromProto(o *storagepb.Object) *ObjectAttrs {
 		Updated:           convertProtoTime(o.GetUpdateTime()),
 		CustomTime:        convertProtoTime(o.GetCustomTime()),
 		ComponentCount:    int64(o.ComponentCount),
+		SoftDeleteTime:    convertProtoTime(o.GetSoftDeleteTime()),
+		HardDeleteTime:    convertProtoTime(o.GetHardDeleteTime()),
 	}
 }
 
@@ -1637,6 +1701,11 @@ type Query struct {
 	// prefixes returned by the query. Only applicable if Delimiter is set to /.
 	// IncludeFoldersAsPrefixes is not yet implemented in the gRPC API.
 	IncludeFoldersAsPrefixes bool
+
+	// SoftDeleted indicates whether to list soft-deleted objects.
+	// If true, only objects that have been soft-deleted will be listed.
+	// By default, soft-deleted objects are not listed.
+	SoftDeleted bool
 }
 
 // attrToFieldMap maps the field names of ObjectAttrs to the underlying field
@@ -1672,6 +1741,8 @@ var attrToFieldMap = map[string]string{
 	"CustomTime":              "customTime",
 	"ComponentCount":          "componentCount",
 	"Retention":               "retention",
+	"HardDeleteTime":          "hardDeleteTime",
+	"SoftDeleteTime":          "softDeleteTime",
 }
 
 // attrToProtoFieldMap maps the field names of ObjectAttrs to the underlying field
@@ -1704,6 +1775,8 @@ var attrToProtoFieldMap = map[string]string{
 	"CustomerKeySHA256":       "customer_encryption",
 	"CustomTime":              "custom_time",
 	"ComponentCount":          "component_count",
+	"HardDeleteTime":          "hard_delete_time",
+	"SoftDeleteTime":          "soft_delete_time",
 	// MediaLink was explicitly excluded from the proto as it is an HTTP-ism.
 	// "MediaLink":               "mediaLink",
 	// TODO: add object retention - b/308194853