mirror of https://github.com/apache/jclouds.git
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:
parent
10262df81c
commit
1e1eb5a092
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -102,6 +102,11 @@ public interface AsyncBlobStore {
|
|||
*/
|
||||
ListenableFuture<Void> deleteContainer(String container);
|
||||
|
||||
/**
|
||||
* @see BlobStore#deleteContainerIfEmpty
|
||||
*/
|
||||
ListenableFuture<Boolean> deleteContainerIfEmpty(String container);
|
||||
|
||||
/**
|
||||
* @see BlobStore#directoryExists
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue