1616
1717package com .google .cloud .storage .it ;
1818
19+ import com .google .api .gax .grpc .GrpcCallContext ;
1920import com .google .api .gax .paging .Page ;
21+ import com .google .api .gax .rpc .ApiException ;
22+ import com .google .api .gax .rpc .FailedPreconditionException ;
2023import com .google .cloud .storage .Blob ;
2124import com .google .cloud .storage .Storage ;
25+ import com .google .cloud .storage .Storage .BlobField ;
2226import com .google .cloud .storage .Storage .BlobListOption ;
2327import com .google .cloud .storage .Storage .BlobSourceOption ;
2428import com .google .cloud .storage .Storage .BucketSourceOption ;
29+ import com .google .common .collect .ImmutableList ;
30+ import com .google .common .collect .ImmutableMap ;
31+ import com .google .storage .control .v2 .BucketName ;
32+ import com .google .storage .control .v2 .DeleteFolderRequest ;
33+ import com .google .storage .control .v2 .DeleteManagedFolderRequest ;
34+ import com .google .storage .control .v2 .Folder ;
35+ import com .google .storage .control .v2 .GetStorageLayoutRequest ;
36+ import com .google .storage .control .v2 .ListFoldersRequest ;
37+ import com .google .storage .control .v2 .ListManagedFoldersRequest ;
38+ import com .google .storage .control .v2 .StorageControlClient ;
39+ import com .google .storage .control .v2 .StorageLayout ;
40+ import com .google .storage .control .v2 .StorageLayoutName ;
41+ import java .util .Collections ;
42+ import java .util .Comparator ;
2543import java .util .List ;
2644import java .util .logging .Level ;
2745import java .util .logging .Logger ;
2846import java .util .stream .Collectors ;
47+ import java .util .stream .Stream ;
2948import java .util .stream .StreamSupport ;
3049
3150public final class BucketCleaner {
@@ -48,12 +67,9 @@ public static void doCleanup(String bucketName, Storage s) {
4867 b .getName (),
4968 s .delete (b .getBlobId (), BlobSourceOption .userProject (projectId ))))
5069 .collect (Collectors .toList ());
51- List <DeleteResult > failedDeletes =
52- deleteResults .stream ().filter (r -> !r .success ).collect (Collectors .toList ());
53- failedDeletes .forEach (
54- r -> LOGGER .warning (String .format ("Failed to delete object %s/%s" , bucketName , r .name )));
70+ boolean anyFailedObjectDeletes = getIfAnyFailedAndReport (bucketName , deleteResults , "object" );
5571
56- if (failedDeletes . isEmpty () ) {
72+ if (! anyFailedObjectDeletes ) {
5773 s .delete (bucketName , BucketSourceOption .userProject (projectId ));
5874 } else {
5975 LOGGER .warning ("Unable to delete bucket due to previous failed object deletes" );
@@ -64,6 +80,161 @@ public static void doCleanup(String bucketName, Storage s) {
6480 }
6581 }
6682
83+ public static void doCleanup (String bucketName , Storage s , StorageControlClient ctrl ) {
84+ LOGGER .warning ("Starting bucket cleanup: " + bucketName );
85+ String projectId = s .getOptions ().getProjectId ();
86+ try {
87+ // TODO: probe bucket existence, a bad test could have deleted the bucket
88+ Page <Blob > page1 =
89+ s .list (
90+ bucketName ,
91+ BlobListOption .userProject (projectId ),
92+ BlobListOption .versions (true ),
93+ BlobListOption .fields (BlobField .NAME ));
94+
95+ List <DeleteResult > objectResults =
96+ StreamSupport .stream (page1 .iterateAll ().spliterator (), false )
97+ .map (
98+ b ->
99+ new DeleteResult (
100+ b .getName (),
101+ s .delete (b .getBlobId (), BlobSourceOption .userProject (projectId ))))
102+ .collect (Collectors .toList ());
103+ boolean anyFailedObjectDelete = getIfAnyFailedAndReport (bucketName , objectResults , "object" );
104+ boolean anyFailedFolderDelete = false ;
105+ boolean anyFailedManagedFolderDelete = false ;
106+
107+ GrpcCallContext grpcCallContext =
108+ GrpcCallContext .createDefault ()
109+ .withExtraHeaders (
110+ ImmutableMap .of ("x-goog-user-project" , ImmutableList .of (projectId )));
111+ if (!anyFailedObjectDelete ) {
112+ BucketName parent = BucketName .of ("_" , bucketName );
113+ StorageLayout storageLayout =
114+ ctrl .getStorageLayoutCallable ()
115+ .call (
116+ GetStorageLayoutRequest .newBuilder ()
117+ .setName (
118+ StorageLayoutName .of (parent .getProject (), parent .getBucket ())
119+ .toString ())
120+ .build (),
121+ grpcCallContext );
122+
123+ List <DeleteResult > folderDeletes ;
124+ if (storageLayout .hasHierarchicalNamespace ()
125+ && storageLayout .getHierarchicalNamespace ().getEnabled ()) {
126+ folderDeletes =
127+ StreamSupport .stream (
128+ ctrl .listFoldersPagedCallable ()
129+ .call (
130+ ListFoldersRequest .newBuilder ().setParent (parent .toString ()).build (),
131+ grpcCallContext )
132+ .iterateAll ()
133+ .spliterator (),
134+ false )
135+ .collect (Collectors .toList ())
136+ .stream ()
137+ .sorted (Collections .reverseOrder (Comparator .comparing (Folder ::getName )))
138+ .map (
139+ folder -> {
140+ String formatted = String .format ("folder = %s" , folder .getName ());
141+ LOGGER .warning (formatted );
142+ boolean success = true ;
143+ try {
144+ ctrl .deleteFolderCallable ()
145+ .call (
146+ DeleteFolderRequest .newBuilder ()
147+ .setName (folder .getName ())
148+ .build (),
149+ grpcCallContext );
150+ } catch (ApiException e ) {
151+ success = false ;
152+ }
153+ return new DeleteResult (folder .getName (), success );
154+ })
155+ .collect (Collectors .toList ());
156+ } else {
157+ folderDeletes = ImmutableList .of ();
158+ }
159+
160+ List <DeleteResult > managedFolderDeletes ;
161+ try {
162+ managedFolderDeletes =
163+ StreamSupport .stream (
164+ ctrl .listManagedFoldersPagedCallable ()
165+ .call (
166+ ListManagedFoldersRequest .newBuilder ()
167+ .setParent (parent .toString ())
168+ .build (),
169+ grpcCallContext )
170+ .iterateAll ()
171+ .spliterator (),
172+ false )
173+ .map (
174+ managedFolder -> {
175+ String formatted =
176+ String .format ("managedFolder = %s" , managedFolder .getName ());
177+ LOGGER .warning (formatted );
178+ boolean success = true ;
179+ try {
180+ ctrl .deleteManagedFolderCallable ()
181+ .call (
182+ DeleteManagedFolderRequest .newBuilder ()
183+ .setName (managedFolder .getName ())
184+ .build (),
185+ grpcCallContext );
186+ } catch (ApiException e ) {
187+ success = false ;
188+ }
189+ return new DeleteResult (managedFolder .getName (), success );
190+ })
191+ .collect (Collectors .toList ());
192+ } catch (FailedPreconditionException fpe ) {
193+ // FAILED_PRECONDITION: Uniform bucket-level access is required to be enabled on the
194+ // bucket in order to perform this operation. Read more at
195+ // https://cloud.google.com/storage/docs/uniform-bucket-level-access
196+ managedFolderDeletes = ImmutableList .of ();
197+ }
198+
199+ anyFailedFolderDelete = getIfAnyFailedAndReport (bucketName , folderDeletes , "folder" );
200+ anyFailedManagedFolderDelete =
201+ getIfAnyFailedAndReport (bucketName , managedFolderDeletes , "managed folder" );
202+ }
203+
204+ List <String > failed =
205+ Stream .of (
206+ anyFailedObjectDelete ? "object" : "" ,
207+ anyFailedFolderDelete ? "folder" : "" ,
208+ anyFailedManagedFolderDelete ? "managed folder" : "" )
209+ .filter (ss -> !ss .isEmpty ())
210+ .collect (Collectors .toList ());
211+
212+ if (!anyFailedObjectDelete && !anyFailedFolderDelete && !anyFailedManagedFolderDelete ) {
213+ s .delete (bucketName , BucketSourceOption .userProject (projectId ));
214+ } else {
215+ LOGGER .warning (
216+ String .format (
217+ "Unable to delete bucket %s due to previous failed %s deletes" ,
218+ bucketName , failed ));
219+ }
220+
221+ LOGGER .warning ("Bucket cleanup complete: " + bucketName );
222+ } catch (Exception e ) {
223+ LOGGER .log (Level .SEVERE , e , () -> "Error during bucket cleanup." );
224+ }
225+ }
226+
227+ private static boolean getIfAnyFailedAndReport (
228+ String bucketName , List <DeleteResult > deleteResults , String resourceType ) {
229+ List <DeleteResult > failedDeletes =
230+ deleteResults .stream ().filter (r -> !r .success ).collect (Collectors .toList ());
231+ failedDeletes .forEach (
232+ r ->
233+ LOGGER .warning (
234+ String .format ("Failed to delete %s %s/%s" , resourceType , bucketName , r .name )));
235+ return !failedDeletes .isEmpty ();
236+ }
237+
67238 private static final class DeleteResult {
68239 private final String name ;
69240 private final boolean success ;
0 commit comments