1313 */
1414package org .geowebcache .s3 ;
1515
16+ import static com .google .common .base .Preconditions .checkArgument ;
1617import static com .google .common .base .Preconditions .checkNotNull ;
18+ import static java .lang .String .format ;
1719import static java .util .Objects .isNull ;
1820
1921import com .amazonaws .AmazonServiceException ;
2022import com .amazonaws .services .s3 .AmazonS3Client ;
2123import com .amazonaws .services .s3 .model .AccessControlList ;
2224import com .amazonaws .services .s3 .model .BucketPolicy ;
2325import com .amazonaws .services .s3 .model .CannedAccessControlList ;
24- import com .amazonaws .services .s3 .model .DeleteObjectsRequest ;
25- import com .amazonaws .services .s3 .model .DeleteObjectsRequest .KeyVersion ;
2626import com .amazonaws .services .s3 .model .Grant ;
2727import com .amazonaws .services .s3 .model .ObjectMetadata ;
2828import com .amazonaws .services .s3 .model .PutObjectRequest ;
2929import com .amazonaws .services .s3 .model .S3Object ;
3030import com .amazonaws .services .s3 .model .S3ObjectInputStream ;
3131import com .amazonaws .services .s3 .model .S3ObjectSummary ;
32- import com .google .common .base .Function ;
33- import com .google .common .collect .AbstractIterator ;
34- import com .google .common .collect .Iterators ;
35- import com .google .common .collect .Lists ;
3632import com .google .common .io .ByteStreams ;
3733import java .io .ByteArrayInputStream ;
3834import java .io .ByteArrayOutputStream ;
4137import java .nio .channels .WritableByteChannel ;
4238import java .util .ArrayList ;
4339import java .util .Arrays ;
44- import java .util .Iterator ;
4540import java .util .List ;
4641import java .util .Map ;
4742import java .util .Objects ;
5045import java .util .Set ;
5146import java .util .logging .Level ;
5247import java .util .logging .Logger ;
48+ import java .util .regex .Matcher ;
49+ import java .util .regex .Pattern ;
5350import java .util .stream .Collectors ;
51+ import java .util .stream .IntStream ;
5452import javax .annotation .Nullable ;
5553import org .geotools .util .logging .Logging ;
5654import org .geowebcache .GeoWebCacheException ;
6159import org .geowebcache .locks .LockProvider ;
6260import org .geowebcache .mime .MimeException ;
6361import org .geowebcache .mime .MimeType ;
62+ import org .geowebcache .s3 .streams .TileDeletionListenerNotifier ;
6463import org .geowebcache .storage .BlobStore ;
6564import org .geowebcache .storage .BlobStoreListener ;
6665import org .geowebcache .storage .BlobStoreListenerList ;
6766import org .geowebcache .storage .CompositeBlobStore ;
6867import org .geowebcache .storage .StorageException ;
6968import org .geowebcache .storage .TileObject ;
7069import org .geowebcache .storage .TileRange ;
71- import org .geowebcache .storage .TileRangeIterator ;
7270import org .geowebcache .util .TMSKeyBuilder ;
7371
7472public class S3BlobStore implements BlobStore {
@@ -83,8 +81,6 @@ public class S3BlobStore implements BlobStore {
8381
8482 private String bucketName ;
8583
86- private volatile boolean shutDown ;
87-
8884 private final S3Ops s3Ops ;
8985
9086 private CannedAccessControlList acl ;
@@ -100,7 +96,7 @@ public S3BlobStore(S3BlobStoreInfo config, TileLayerDispatcher layers, LockProvi
10096 conn = validateClient (config .buildClient (), bucketName );
10197 acl = config .getAccessControlList ();
10298
103- this .s3Ops = new S3Ops (conn , bucketName , keyBuilder , lockProvider );
99+ this .s3Ops = new S3Ops (conn , bucketName , keyBuilder , lockProvider , listeners );
104100
105101 boolean empty = !s3Ops .prefixExists (prefix );
106102 boolean existing = Objects .nonNull (s3Ops .getObjectMetadata (keyBuilder .storeMetadata ()));
@@ -172,7 +168,6 @@ private void checkBucketPolicy(AmazonS3Client client, String bucketName) throws
172168
173169 @ Override
174170 public void destroy () {
175- this .shutDown = true ;
176171 AmazonS3Client conn = this .conn ;
177172 this .conn = null ;
178173 if (conn != null ) {
@@ -279,80 +274,40 @@ public boolean get(TileObject obj) throws StorageException {
279274 return true ;
280275 }
281276
282- private class TileToKey implements Function <long [], KeyVersion > {
283-
284- private final String coordsPrefix ;
285-
286- private final String extension ;
287-
288- public TileToKey (String coordsPrefix , MimeType mimeType ) {
289- this .coordsPrefix = coordsPrefix ;
290- this .extension = mimeType .getInternalName ();
291- }
292-
293- @ Override
294- public KeyVersion apply (long [] loc ) {
295- long z = loc [2 ];
296- long x = loc [0 ];
297- long y = loc [1 ];
298- StringBuilder sb = new StringBuilder (coordsPrefix );
299- sb .append (z ).append ('/' ).append (x ).append ('/' ).append (y ).append ('.' ).append (extension );
300- return new KeyVersion (sb .toString ());
301- }
302- }
303-
304277 @ Override
305278 public boolean delete (final TileRange tileRange ) throws StorageException {
279+ checkNotNull (tileRange , "tile range must not be null" );
280+ checkArgument (tileRange .getZoomStart () >= 0 , "zoom start must be greater or equal than zero" );
281+ checkArgument (
282+ tileRange .getZoomStop () >= tileRange .getZoomStart (),
283+ "zoom stop must be greater or equal than start zoom" );
306284
307285 final String coordsPrefix = keyBuilder .coordinatesPrefix (tileRange , true );
308286 if (!s3Ops .prefixExists (coordsPrefix )) {
309287 return false ;
310288 }
311289
312- final Iterator <long []> tileLocations = new AbstractIterator <>() {
313-
314- // TileRange iterator with 1x1 meta tiling factor
315- private TileRangeIterator trIter = new TileRangeIterator (tileRange , new int [] {1 , 1 });
290+ // Create a prefix for each zoom level
291+ long count = IntStream .range (tileRange .getZoomStart (), tileRange .getZoomStop () + 1 )
292+ .mapToObj (level -> scheduleDeleteForZoomLevel (tileRange , level ))
293+ .filter (Objects ::nonNull )
294+ .count ();
316295
317- @ Override
318- protected long [] computeNext () {
319- long [] gridLoc = trIter .nextMetaGridLocation (new long [3 ]);
320- return gridLoc == null ? endOfData () : gridLoc ;
321- }
322- };
323-
324- if (listeners .isEmpty ()) {
325- // if there are no listeners, don't bother requesting every tile
326- // metadata to notify the listeners
327- Iterator <List <long []>> partition = Iterators .partition (tileLocations , 1000 );
328- final TileToKey tileToKey = new TileToKey (coordsPrefix , tileRange .getMimeType ());
329-
330- while (partition .hasNext () && !shutDown ) {
331- List <long []> locations = partition .next ();
332- List <KeyVersion > keys = Lists .transform (locations , tileToKey );
333-
334- DeleteObjectsRequest req = new DeleteObjectsRequest (bucketName );
335- req .setQuiet (true );
336- req .setKeys (keys );
337- conn .deleteObjects (req );
338- }
296+ // Check all ranges where scheduled
297+ return count == (tileRange .getZoomStop () - tileRange .getZoomStart () + 1 );
298+ }
339299
340- } else {
341- long [] xyz ;
342- String layerName = tileRange .getLayerName ();
343- String gridSetId = tileRange .getGridSetId ();
344- String format = tileRange .getMimeType ().getFormat ();
345- Map <String , String > parameters = tileRange .getParameters ();
346-
347- while (tileLocations .hasNext ()) {
348- xyz = tileLocations .next ();
349- TileObject tile = TileObject .createQueryTileObject (layerName , xyz , gridSetId , format , parameters );
350- tile .setParametersId (tileRange .getParametersId ());
351- delete (tile );
352- }
300+ private String scheduleDeleteForZoomLevel (TileRange tileRange , int level ) {
301+ String zoomPath = keyBuilder .forZoomLevel (tileRange , level );
302+ Bounds bounds = new Bounds (tileRange .rangeBounds (level ));
303+ String prefix = format ("%s?%s" , zoomPath , bounds );
304+ try {
305+ s3Ops .scheduleAsyncDelete (prefix );
306+ return prefix ;
307+ } catch (GeoWebCacheException e ) {
308+ log .warning ("Cannot schedule delete for prefix " + prefix );
309+ return null ;
353310 }
354-
355- return true ;
356311 }
357312
358313 @ Override
@@ -457,8 +412,7 @@ private Properties getLayerMetadata(String layerName) {
457412 }
458413
459414 private void putParametersMetadata (String layerName , String parametersId , Map <String , String > parameters ) {
460- assert (isNull (parametersId ) == isNull (parameters ));
461- if (isNull (parametersId )) {
415+ if (isNull (parameters )) {
462416 return ;
463417 }
464418 Properties properties = new Properties ();
@@ -519,4 +473,63 @@ public Map<String, Optional<Map<String, String>>> getParametersMapping(String la
519473 .map (props -> (Map <String , String >) (Map <?, ?>) props )
520474 .collect (Collectors .toMap (ParametersUtils ::getId , Optional ::of ));
521475 }
476+
477+ public static class Bounds {
478+ private static final Pattern boundsRegex =
479+ Pattern .compile ("^(?<prefix>.*/)\\ ?bounds=(?<minx>\\ d+),(?<miny>\\ d+),(?<maxx>\\ d+),(?<maxy>\\ d+)$" );
480+ private final long minX , minY , maxX , maxY ;
481+
482+ public Bounds (long [] bound ) {
483+ minX = Math .min (bound [0 ], bound [2 ]);
484+ minY = Math .min (bound [1 ], bound [3 ]);
485+ maxX = Math .max (bound [0 ], bound [2 ]);
486+ maxY = Math .max (bound [1 ], bound [3 ]);
487+ }
488+
489+ public long getMinX () {
490+ return minX ;
491+ }
492+
493+ public long getMaxX () {
494+ return maxX ;
495+ }
496+
497+ static Optional <Bounds > createBounds (String prefix ) {
498+ Matcher matcher = boundsRegex .matcher (prefix );
499+ if (!matcher .matches ()) {
500+ return Optional .empty ();
501+ }
502+
503+ Bounds bounds = new Bounds (new long [] {
504+ Long .parseLong (matcher .group ("minx" )),
505+ Long .parseLong (matcher .group ("miny" )),
506+ Long .parseLong (matcher .group ("maxx" )),
507+ Long .parseLong (matcher .group ("maxy" ))
508+ });
509+ return Optional .of (bounds );
510+ }
511+
512+ static String prefixWithoutBounds (String prefix ) {
513+ Matcher matcher = boundsRegex .matcher (prefix );
514+ if (matcher .matches ()) {
515+ return matcher .group ("prefix" );
516+ }
517+ return prefix ;
518+ }
519+
520+ @ Override
521+ public String toString () {
522+ return format ("bounds=%d,%d,%d,%d" , minX , minY , maxX , maxY );
523+ }
524+
525+ public boolean predicate (S3ObjectSummary s3ObjectSummary ) {
526+ var matcher = TileDeletionListenerNotifier .keyRegex .matcher (s3ObjectSummary .getKey ());
527+ if (!matcher .matches ()) {
528+ return false ;
529+ }
530+ long x = Long .parseLong (matcher .group (TileDeletionListenerNotifier .X_GROUP_POS ));
531+ long y = Long .parseLong (matcher .group (TileDeletionListenerNotifier .Y_GROUP_POS ));
532+ return x >= minX && x <= maxX && y >= minY && y <= maxY ;
533+ }
534+ }
522535}
0 commit comments