|
@@ -67,10 +67,13 @@ import org.apache.hadoop.fs.FileAlreadyExistsException;
|
|
|
import org.apache.hadoop.fs.FileStatus;
|
|
|
import org.apache.hadoop.fs.FileSystem;
|
|
|
import org.apache.hadoop.fs.GlobalStorageStatistics;
|
|
|
+import org.apache.hadoop.fs.InvalidRequestException;
|
|
|
import org.apache.hadoop.fs.LocalFileSystem;
|
|
|
import org.apache.hadoop.fs.LocatedFileStatus;
|
|
|
import org.apache.hadoop.fs.Path;
|
|
|
import org.apache.hadoop.fs.PathFilter;
|
|
|
+import org.apache.hadoop.fs.PathIOException;
|
|
|
+import org.apache.hadoop.fs.PathIsNotEmptyDirectoryException;
|
|
|
import org.apache.hadoop.fs.RemoteIterator;
|
|
|
import org.apache.hadoop.fs.StorageStatistics;
|
|
|
import org.apache.hadoop.fs.permission.FsPermission;
|
|
@@ -803,12 +806,26 @@ public class S3AFileSystem extends FileSystem {
|
|
|
* operation statistics.
|
|
|
* @param key key to blob to delete.
|
|
|
*/
|
|
|
- private void deleteObject(String key) {
|
|
|
+ private void deleteObject(String key) throws InvalidRequestException {
|
|
|
+ blockRootDelete(key);
|
|
|
incrementWriteOperations();
|
|
|
incrementStatistic(OBJECT_DELETE_REQUESTS);
|
|
|
s3.deleteObject(bucket, key);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Reject any request to delete an object where the key is root.
|
|
|
+ * @param key key to validate
|
|
|
+ * @throws InvalidRequestException if the request was rejected due to
|
|
|
+ * a mistaken attempt to delete the root directory.
|
|
|
+ */
|
|
|
+ private void blockRootDelete(String key) throws InvalidRequestException {
|
|
|
+ if (key.isEmpty() || "/".equals(key)) {
|
|
|
+ throw new InvalidRequestException("Bucket "+ bucket
|
|
|
+ +" cannot be deleted");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* Perform a bulk object delete operation.
|
|
|
* Increments the {@code OBJECT_DELETE_REQUESTS} and write
|
|
@@ -948,17 +965,24 @@ public class S3AFileSystem extends FileSystem {
|
|
|
/**
|
|
|
* A helper method to delete a list of keys on a s3-backend.
|
|
|
*
|
|
|
- * @param keysToDelete collection of keys to delete on the s3-backend
|
|
|
+ * @param keysToDelete collection of keys to delete on the s3-backend.
|
|
|
+ * if empty, no request is made of the object store.
|
|
|
* @param clearKeys clears the keysToDelete-list after processing the list
|
|
|
* when set to true
|
|
|
* @param deleteFakeDir indicates whether this is for deleting fake dirs
|
|
|
+ * @throws InvalidRequestException if the request was rejected due to
|
|
|
+ * a mistaken attempt to delete the root directory.
|
|
|
*/
|
|
|
private void removeKeys(List<DeleteObjectsRequest.KeyVersion> keysToDelete,
|
|
|
- boolean clearKeys, boolean deleteFakeDir) throws AmazonClientException {
|
|
|
+ boolean clearKeys, boolean deleteFakeDir)
|
|
|
+ throws AmazonClientException, InvalidRequestException {
|
|
|
if (keysToDelete.isEmpty()) {
|
|
|
- // no keys
|
|
|
+ // exit fast if there are no keys to delete
|
|
|
return;
|
|
|
}
|
|
|
+ for (DeleteObjectsRequest.KeyVersion keyVersion : keysToDelete) {
|
|
|
+ blockRootDelete(keyVersion.getKey());
|
|
|
+ }
|
|
|
if (enableMultiObjectsDelete) {
|
|
|
deleteObjects(new DeleteObjectsRequest(bucket).withKeys(keysToDelete));
|
|
|
} else {
|
|
@@ -1020,18 +1044,16 @@ public class S3AFileSystem extends FileSystem {
|
|
|
if (status.isDirectory()) {
|
|
|
LOG.debug("delete: Path is a directory: {}", f);
|
|
|
|
|
|
- if (!recursive && !status.isEmptyDirectory()) {
|
|
|
- throw new IOException("Path is a folder: " + f +
|
|
|
- " and it is not an empty directory");
|
|
|
- }
|
|
|
-
|
|
|
if (!key.endsWith("/")) {
|
|
|
key = key + "/";
|
|
|
}
|
|
|
|
|
|
if (key.equals("/")) {
|
|
|
- LOG.info("s3a cannot delete the root directory");
|
|
|
- return false;
|
|
|
+ return rejectRootDirectoryDelete(status, recursive);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!recursive && !status.isEmptyDirectory()) {
|
|
|
+ throw new PathIsNotEmptyDirectoryException(f.toString());
|
|
|
}
|
|
|
|
|
|
if (status.isEmptyDirectory()) {
|
|
@@ -1072,10 +1094,39 @@ public class S3AFileSystem extends FileSystem {
|
|
|
deleteObject(key);
|
|
|
}
|
|
|
|
|
|
- createFakeDirectoryIfNecessary(f.getParent());
|
|
|
+ Path parent = f.getParent();
|
|
|
+ if (parent != null) {
|
|
|
+ createFakeDirectoryIfNecessary(parent);
|
|
|
+ }
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Implements the specific logic to reject root directory deletion.
|
|
|
+ * The caller must return the result of this call, rather than
|
|
|
+ * attempt to continue with the delete operation: deleting root
|
|
|
+ * directories is never allowed. This method simply implements
|
|
|
+ * the policy of when to return an exit code versus raise an exception.
|
|
|
+ * @param status filesystem status
|
|
|
+ * @param recursive recursive flag from command
|
|
|
+ * @return a return code for the operation
|
|
|
+ * @throws PathIOException if the operation was explicitly rejected.
|
|
|
+ */
|
|
|
+ private boolean rejectRootDirectoryDelete(S3AFileStatus status,
|
|
|
+ boolean recursive) throws IOException {
|
|
|
+ LOG.info("s3a delete the {} root directory of {}", bucket, recursive);
|
|
|
+ boolean emptyRoot = status.isEmptyDirectory();
|
|
|
+ if (emptyRoot) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ if (recursive) {
|
|
|
+ return false;
|
|
|
+ } else {
|
|
|
+ // reject
|
|
|
+ throw new PathIOException(bucket, "Cannot delete root path");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private void createFakeDirectoryIfNecessary(Path f)
|
|
|
throws IOException, AmazonClientException {
|
|
|
String key = pathToKey(f);
|
|
@@ -1551,7 +1602,7 @@ public class S3AFileSystem extends FileSystem {
|
|
|
}
|
|
|
try {
|
|
|
removeKeys(keysToRemove, false, true);
|
|
|
- } catch(AmazonClientException e) {
|
|
|
+ } catch(AmazonClientException | InvalidRequestException e) {
|
|
|
instrumentation.errorIgnored();
|
|
|
if (LOG.isDebugEnabled()) {
|
|
|
StringBuilder sb = new StringBuilder();
|