Add deleteContainerIfEmpty to BlobStore

This matches how most blobstores operate: delete container is a single
operation, not a compound operation which recursively deletes blobs.
Azure is the only provider which allows deleting a non-empty
container.
This commit is contained in:
Andrew Gaul 2014-07-22 13:30:03 -07:00
parent 10262df81c
commit 1e1eb5a092
15 changed files with 180 additions and 26 deletions

View File

@ -35,13 +35,13 @@ import javax.ws.rs.core.MediaType;
import org.jclouds.Fallbacks.FalseOnNotFoundOr404;
import org.jclouds.Fallbacks.NullOnNotFoundOr404;
import org.jclouds.Fallbacks.VoidOnNotFoundOr404;
import org.jclouds.atmos.binders.BindMetadataToHeaders;
import org.jclouds.atmos.domain.AtmosObject;
import org.jclouds.atmos.domain.BoundedSet;
import org.jclouds.atmos.domain.DirectoryEntry;
import org.jclouds.atmos.domain.SystemMetadata;
import org.jclouds.atmos.domain.UserMetadata;
import org.jclouds.atmos.fallbacks.TrueOn404FalseOnPathNotEmpty;
import org.jclouds.atmos.filters.SignRequest;
import org.jclouds.atmos.functions.AtmosObjectName;
import org.jclouds.atmos.functions.ParseDirectoryListFromContentAndHeaders;
@ -201,10 +201,10 @@ public interface AtmosAsyncClient extends Closeable {
*/
@Named("DeleteObject")
@DELETE
@Fallback(VoidOnNotFoundOr404.class)
@Fallback(TrueOn404FalseOnPathNotEmpty.class)
@Path("/{path}")
@Consumes(MediaType.WILDCARD)
ListenableFuture<Void> deletePath(@PathParam("path") String path);
ListenableFuture<Boolean> deletePath(@PathParam("path") String path);
/**
* @see AtmosClient#pathExists

View File

@ -59,9 +59,11 @@ import org.jclouds.domain.Location;
import org.jclouds.http.options.GetOptions;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Supplier;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
@ -260,7 +262,8 @@ public class AtmosAsyncBlobStore extends BaseAsyncBlobStore {
*/
@Override
public ListenableFuture<Void> removeBlob(String container, String key) {
return async.deletePath(container + "/" + key);
return Futures.transform(async.deletePath(container + "/" + key), Functions.constant((Void) null),
userExecutor);
}
@Override

View File

@ -0,0 +1,47 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.atmos.fallbacks;
import static com.google.common.base.Throwables.propagate;
import static com.google.common.util.concurrent.Futures.immediateFuture;
import static org.jclouds.util.Throwables2.getFirstThrowableOfType;
import org.jclouds.Fallback;
import org.jclouds.atmos.AtmosResponseException;
import org.jclouds.atmos.reference.AtmosErrorCode;
import org.jclouds.http.HttpUtils;
import com.google.common.util.concurrent.ListenableFuture;
public final class TrueOn404FalseOnPathNotEmpty implements Fallback<Boolean> {
@Override
public ListenableFuture<Boolean> create(Throwable t) throws Exception {
return immediateFuture(createOrPropagate(t));
}
@Override
public Boolean createOrPropagate(Throwable t) throws Exception {
if (HttpUtils.contains404(t)) {
return true;
}
AtmosResponseException exception = getFirstThrowableOfType(t, AtmosResponseException.class);
if (exception != null && exception.getError().getCode() == AtmosErrorCode.DIRECTORY_NOT_EMPTY.getCode()) {
return false;
}
throw propagate(t);
}
}

View File

@ -23,11 +23,11 @@ import java.io.IOException;
import org.jclouds.Fallbacks.FalseOnNotFoundOr404;
import org.jclouds.Fallbacks.NullOnNotFoundOr404;
import org.jclouds.Fallbacks.VoidOnNotFoundOr404;
import org.jclouds.apis.ApiMetadata;
import org.jclouds.atmos.blobstore.functions.BlobToObject;
import org.jclouds.atmos.config.AtmosRestClientModule;
import org.jclouds.atmos.domain.AtmosObject;
import org.jclouds.atmos.fallbacks.TrueOn404FalseOnPathNotEmpty;
import org.jclouds.atmos.filters.SignRequest;
import org.jclouds.atmos.functions.ParseDirectoryListFromContentAndHeaders;
import org.jclouds.atmos.functions.ParseNullableURIFromListOrLocationHeaderIf20x;
@ -44,6 +44,7 @@ import org.jclouds.date.TimeStamp;
import org.jclouds.http.HttpRequest;
import org.jclouds.http.functions.ParseURIFromListOrLocationHeaderIf20x;
import org.jclouds.http.functions.ReleasePayloadAndReturn;
import org.jclouds.http.functions.ReturnTrueIf2xx;
import org.jclouds.http.options.GetOptions;
import org.jclouds.rest.ConfiguresRestClient;
import org.jclouds.rest.internal.BaseAsyncClientTest;
@ -264,9 +265,9 @@ public class AtmosAsyncClientTest extends BaseAsyncClientTest<AtmosAsyncClient>
assertNonPayloadHeadersEqual(request, HttpHeaders.ACCEPT + ": */*\n");
assertPayloadEquals(request, null, null, false);
assertResponseParserClassEquals(method, request, ReleasePayloadAndReturn.class);
assertResponseParserClassEquals(method, request, ReturnTrueIf2xx.class);
assertSaxResponseParserClassEquals(method, null);
assertFallbackClassEquals(method, VoidOnNotFoundOr404.class);
assertFallbackClassEquals(method, TrueOn404FalseOnPathNotEmpty.class);
checkFilters(request);
}

View File

@ -49,6 +49,7 @@ import org.jclouds.http.options.GetOptions;
import org.jclouds.lifecycle.Closer;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Throwables;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
@ -134,21 +135,14 @@ public class StubAtmosAsyncClient implements AtmosAsyncClient {
}
@Override
public ListenableFuture<Void> deletePath(String path) {
public ListenableFuture<Boolean> deletePath(String path) {
if (path.indexOf('/') == path.length() - 1) {
// chop off the trailing slash
return Futures.transform(blobStore.deleteContainerIfEmpty(path.substring(0, path.length() - 1)),
new Function<Boolean, Void>() {
public Void apply(Boolean from) {
return null;
}
}, userExecutor);
return blobStore.deleteContainerIfEmpty(path.substring(0, path.length() - 1));
} else {
String container = path.substring(0, path.indexOf('/'));
path = path.substring(path.indexOf('/') + 1);
return blobStore.removeBlob(container, path);
return Futures.transform(blobStore.removeBlob(container, path), Functions.constant(Boolean.TRUE), userExecutor);
}
}

View File

@ -99,6 +99,24 @@ public class FilesystemContainerIntegrationTest extends BaseContainerIntegration
super.deleteContainerWithContents();
}
@Override
@Test(dataProvider = "ignoreOnWindows")
public void deleteContainerWithoutContents() throws InterruptedException {
super.deleteContainerWithoutContents();
}
@Override
@Test(dataProvider = "ignoreOnWindows")
public void deleteContainerIfEmptyWithContents() throws InterruptedException {
super.deleteContainerIfEmptyWithContents();
}
@Override
@Test(dataProvider = "ignoreOnWindows")
public void deleteContainerIfEmptyWithoutContents() throws InterruptedException {
super.deleteContainerIfEmptyWithoutContents();
}
@Override
@Test(dataProvider = "ignoreOnWindows")
public void testListContainer() throws InterruptedException, ExecutionException, TimeoutException {
@ -129,12 +147,6 @@ public class FilesystemContainerIntegrationTest extends BaseContainerIntegration
super.testPutTwiceIsOkAndDoesntOverwrite();
}
@Override
@Test(dataProvider = "ignoreOnWindows")
public void deleteContainerIfEmpty() throws InterruptedException {
super.deleteContainerIfEmpty();
}
@Override
@Test(dataProvider = "ignoreOnWindows")
public void testListContainerMaxResults() throws InterruptedException {

View File

@ -207,6 +207,11 @@ Options can also be specified for extension modules
[^BlobStore blobstore container-name]
(.deleteContainer blobstore container-name))
(defn delete-container-if-empty
"Delete a container if empty."
[^BlobStore blobstore container-name]
(.deleteContainerIfEmpty blobstore container-name))
(defn container-exists?
"Predicate to check presence of a container"
[^BlobStore blobstore container-name]

View File

@ -102,6 +102,11 @@ public interface AsyncBlobStore {
*/
ListenableFuture<Void> deleteContainer(String container);
/**
* @see BlobStore#deleteContainerIfEmpty
*/
ListenableFuture<Boolean> deleteContainerIfEmpty(String container);
/**
* @see BlobStore#directoryExists
*/

View File

@ -140,9 +140,18 @@ public interface BlobStore {
*
* @param container
* what to delete
* @param container name of the container to delete
*/
void deleteContainer(String container);
/**
* Deletes a container if it is empty.
*
* @param container name of the container to delete
* @return true if the container was deleted or does not exist
*/
boolean deleteContainerIfEmpty(String container);
/**
* Determines if a directory exists
*

View File

@ -256,6 +256,7 @@ public class LocalAsyncBlobStore extends BaseAsyncBlobStore {
return immediateFuture(null);
}
@Override
public ListenableFuture<Boolean> deleteContainerIfEmpty(final String container) {
Boolean returnVal = true;
if (storageStrategy.containerExists(container)) {

View File

@ -266,6 +266,21 @@ public abstract class BaseAsyncBlobStore implements AsyncBlobStore {
});
}
@Override
public ListenableFuture<Boolean> deleteContainerIfEmpty(final String container) {
return userExecutor.submit(new Callable<Boolean>() {
public Boolean call() throws Exception {
return deleteAndVerifyContainerGone(container);
}
@Override
public String toString() {
return "deleteContainerIfEmpty(" + container + ")";
}
});
}
protected void deletePathAndEnsureGone(String path) {
checkState(retry(new Predicate<String>() {
public boolean apply(String in) {
@ -284,6 +299,12 @@ public abstract class BaseAsyncBlobStore implements AsyncBlobStore {
return Futures.<Set<? extends Location>> immediateFuture(locations.get());
}
/**
* Delete a container if it is empty.
*
* @param container what to delete
* @return true if the container was deleted or does not exist
*/
protected abstract boolean deleteAndVerifyContainerGone(String container);
}

View File

@ -189,6 +189,15 @@ public abstract class BaseBlobStore implements BlobStore {
deletePathAndEnsureGone(container);
}
@Override
public boolean deleteContainerIfEmpty(String container) {
try {
return deleteAndVerifyContainerGone(container);
} catch (ContainerNotFoundException cnfe) {
return true;
}
}
protected void deletePathAndEnsureGone(String path) {
checkState(retry(new Predicate<String>() {
public boolean apply(String in) {
@ -207,6 +216,12 @@ public abstract class BaseBlobStore implements BlobStore {
return locations.get();
}
/**
* Delete a container if it is empty.
*
* @param container what to delete
* @return whether container was deleted
*/
protected abstract boolean deleteAndVerifyContainerGone(String container);
}

View File

@ -24,6 +24,8 @@ import static org.jclouds.blobstore.options.ListContainerOptions.Builder.afterMa
import static org.jclouds.blobstore.options.ListContainerOptions.Builder.inDirectory;
import static org.jclouds.blobstore.options.ListContainerOptions.Builder.maxResults;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
import java.io.IOException;
import java.util.Set;
@ -280,7 +282,7 @@ public class BaseContainerIntegrationTest extends BaseBlobStoreIntegrationTest {
}
@Test(groups = { "integration", "live" })
public void deleteContainerIfEmpty() throws InterruptedException {
public void deleteContainerWithoutContents() throws InterruptedException {
final String containerName = getContainerName();
try {
view.getBlobStore().deleteContainer(containerName);
@ -291,6 +293,32 @@ public class BaseContainerIntegrationTest extends BaseBlobStoreIntegrationTest {
}
}
@Test(groups = { "integration", "live" })
public void deleteContainerIfEmptyWithContents() throws InterruptedException {
String containerName = getContainerName();
try {
addBlobToContainer(containerName, "test");
assertFalse(view.getBlobStore().deleteContainerIfEmpty(containerName));
assertTrue(view.getBlobStore().containerExists(containerName));
} finally {
recycleContainerAndAddToPool(containerName);
}
}
@Test(groups = { "integration", "live" })
public void deleteContainerIfEmptyWithoutContents() throws InterruptedException {
final String containerName = getContainerName();
try {
assertTrue(view.getBlobStore().deleteContainerIfEmpty(containerName));
assertNotExists(containerName);
// verify that false is returned even if the container does not exist
assertTrue(view.getBlobStore().deleteContainerIfEmpty(containerName));
} finally {
// this container is now deleted, so we can't reuse it directly
recycleContainerAndAddToPool(containerName);
}
}
@Test(groups = { "integration", "live" })
public void testListContainer() throws InterruptedException, ExecutionException, TimeoutException {
String containerName = getContainerName();

View File

@ -64,6 +64,7 @@ import org.jclouds.http.options.GetOptions;
import com.google.common.base.Function;
import com.google.common.base.Supplier;
import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import org.jclouds.io.Payload;
@ -278,7 +279,13 @@ public class AzureAsyncBlobStore extends BaseAsyncBlobStore {
@Override
protected boolean deleteAndVerifyContainerGone(String container) {
throw new UnsupportedOperationException("please use deleteContainer");
// Azure deleteContainer supports deleting empty containers so emulate
// deleteIfEmpty by listing.
if (!Futures.getUnchecked(list(container)).isEmpty()) {
return false;
}
Futures.getUnchecked(async.deleteContainer(container));
return true;
}
@Override

View File

@ -266,7 +266,13 @@ public class AzureBlobStore extends BaseBlobStore {
@Override
protected boolean deleteAndVerifyContainerGone(String container) {
throw new UnsupportedOperationException("please use deleteContainer");
// Azure deleteContainer supports deleting empty containers so emulate
// deleteIfEmpty by listing.
if (!list(container).isEmpty()) {
return false;
}
sync.deleteContainer(container);
return true;
}
@Override