Skip to content

Commit bdd785e

Browse files
authored
feat: implement CloudStorageFileSystemProvider.move method using the moveBlob API (#1610)
1 parent c4bb644 commit bdd785e

File tree

4 files changed

+150
-16
lines changed

4 files changed

+150
-16
lines changed

google-cloud-nio/src/main/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemProvider.java

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import com.google.cloud.storage.Storage;
3737
import com.google.cloud.storage.Storage.BlobGetOption;
3838
import com.google.cloud.storage.Storage.BlobSourceOption;
39+
import com.google.cloud.storage.Storage.BlobTargetOption;
3940
import com.google.cloud.storage.StorageException;
4041
import com.google.cloud.storage.StorageOptions;
4142
import com.google.common.annotations.VisibleForTesting;
@@ -564,16 +565,92 @@ public void delete(Path path) throws IOException {
564565
@Override
565566
public void move(Path source, Path target, CopyOption... options) throws IOException {
566567
initStorage();
568+
569+
boolean replaceExisting = false;
570+
boolean atomicMove = false;
571+
boolean hasCloudStorageOptions = false;
567572
for (CopyOption option : options) {
568-
if (option == StandardCopyOption.ATOMIC_MOVE) {
573+
if (option instanceof StandardCopyOption) {
574+
switch ((StandardCopyOption) option) {
575+
case COPY_ATTRIBUTES:
576+
// The Objects: move API copies attributes by default.
577+
break;
578+
case REPLACE_EXISTING:
579+
replaceExisting = true;
580+
break;
581+
case ATOMIC_MOVE:
582+
atomicMove = true;
583+
break;
584+
default:
585+
throw new UnsupportedOperationException(option.toString());
586+
}
587+
}
588+
hasCloudStorageOptions = option instanceof CloudStorageOption;
589+
}
590+
591+
CloudStoragePath fromPath = CloudStorageUtil.checkPath(source);
592+
if (fromPath.seemsLikeADirectoryAndUsePseudoDirectories(null)) {
593+
throw new CloudStoragePseudoDirectoryException(fromPath);
594+
}
595+
CloudStoragePath toPath = CloudStorageUtil.checkPath(target);
596+
if (toPath.seemsLikeADirectoryAndUsePseudoDirectories(null)) {
597+
throw new CloudStoragePseudoDirectoryException(toPath);
598+
}
599+
if (fromPath.seemsLikeADirectory() && toPath.seemsLikeADirectory()) {
600+
if (fromPath.getFileSystem().config().usePseudoDirectories()
601+
&& toPath.getFileSystem().config().usePseudoDirectories()) {
602+
// NOOP: This would normally create an empty directory.
603+
return;
604+
} else {
605+
checkArgument(
606+
!fromPath.getFileSystem().config().usePseudoDirectories()
607+
&& !toPath.getFileSystem().config().usePseudoDirectories(),
608+
"File systems associated with paths don't agree on pseudo-directories.");
609+
}
610+
}
611+
boolean crossBucketMove = !fromPath.bucket().equals(toPath.bucket());
612+
if (atomicMove) {
613+
if (hasCloudStorageOptions) {
614+
throw new AtomicMoveNotSupportedException(
615+
source.toString(),
616+
target.toString(),
617+
"Cloud Storage does not support atomic move operations with CloudStorageOptions.");
618+
}
619+
if (crossBucketMove) {
569620
throw new AtomicMoveNotSupportedException(
570621
source.toString(),
571622
target.toString(),
572-
"Google Cloud Storage does not support atomic move operations.");
623+
"Cloud Storage does not support atomic move operations between buckets.");
624+
}
625+
} else if (hasCloudStorageOptions || crossBucketMove) {
626+
// Fall back to copy and delete if atomic move is not possible.
627+
copy(source, target, options);
628+
delete(source);
629+
return;
630+
}
631+
632+
Storage.MoveBlobRequest.Builder builder =
633+
Storage.MoveBlobRequest.newBuilder()
634+
.setSource(fromPath.getBlobId())
635+
.setTarget(toPath.getBlobId());
636+
if (!replaceExisting) {
637+
builder.setTargetOptions(BlobTargetOption.doesNotExist());
638+
}
639+
Storage.MoveBlobRequest request = builder.build();
640+
CloudStorageRetryHandler retryHandler =
641+
new CloudStorageRetryHandler(fromPath.getFileSystem().config());
642+
while (true) {
643+
try {
644+
storage.moveBlob(request);
645+
break;
646+
} catch (StorageException e) {
647+
try {
648+
retryHandler.handleStorageException(e);
649+
} catch (StorageException retriesExhaustedException) {
650+
throw asIoException(retriesExhaustedException, true);
651+
}
573652
}
574653
}
575-
copy(source, target, options);
576-
delete(source);
577654
}
578655

579656
@Override

google-cloud-nio/src/main/java/com/google/cloud/storage/contrib/nio/testing/FakeStorageRpc.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,34 @@ public RewriteResponse openRewrite(RewriteRequest rewriteRequest) throws Storage
465465
data.length);
466466
}
467467

468+
@Override
469+
public StorageObject moveObject(
470+
String bucket,
471+
String sourceObject,
472+
String destinationObject,
473+
Map<StorageRpc.Option, ?> sourceOptions,
474+
Map<StorageRpc.Option, ?> targetOptions)
475+
throws StorageException {
476+
// This logic doesn't exactly match the semantics of the Objects: move API. But it should be
477+
// close enough for the test.
478+
String sourceKey = fullname(bucket, sourceObject);
479+
if (!contents.containsKey(sourceKey)) {
480+
throw new StorageException(NOT_FOUND, "File not found: " + sourceKey);
481+
}
482+
String destKey = fullname(bucket, destinationObject);
483+
StorageObject sourceMetadata = metadata.get(sourceKey);
484+
DateTime currentTime = now();
485+
sourceMetadata.setGeneration(sourceMetadata.getGeneration() + 1);
486+
sourceMetadata.setTimeCreated(currentTime);
487+
sourceMetadata.setUpdated(currentTime);
488+
byte[] sourceData = contents.get(sourceKey);
489+
metadata.put(destKey, sourceMetadata);
490+
contents.put(destKey, Arrays.copyOf(sourceData, sourceData.length));
491+
metadata.remove(sourceKey);
492+
contents.remove(sourceKey);
493+
return sourceMetadata;
494+
}
495+
468496
private static DateTime now() {
469497
return DateTime.parseRfc3339(RFC_3339_FORMATTER.format(new Date()));
470498
}

google-cloud-nio/src/test/java/com/google/cloud/storage/contrib/nio/CloudStorageFileSystemProviderTest.java

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -640,36 +640,45 @@ public void testCopy_atomic_throwsUnsupported() throws Exception {
640640
}
641641

642642
@Test
643-
public void testMove() throws Exception {
644-
Path source = Paths.get(URI.create("gs://military/fashion.show"));
643+
public void testMove_atomic() throws Exception {
644+
Path source = Paths.get(URI.create("gs://greenbean/fashion.show"));
645645
Path target = Paths.get(URI.create("gs://greenbean/adipose"));
646646
Files.write(source, "(✿◕ ‿◕ )ノ".getBytes(UTF_8));
647-
Files.move(source, target);
648-
assertThat(new String(readAllBytes(target), UTF_8)).isEqualTo("(✿◕ ‿◕ )ノ");
647+
Files.move(source, target, ATOMIC_MOVE);
649648
assertThat(Files.exists(source)).isFalse();
650649
assertThat(Files.exists(target)).isTrue();
651650
}
652651

653652
@Test
654-
public void testCreateDirectory() throws Exception {
655-
Path path = Paths.get(URI.create("gs://greenbean/dir/"));
656-
Files.createDirectory(path);
657-
assertThat(Files.exists(path)).isTrue();
653+
public void testMove_crossBucket() throws Exception {
654+
Path source = Paths.get(URI.create("gs://military/fashion.show"));
655+
Path target = Paths.get(URI.create("gs://greenbean/adipose"));
656+
Files.write(source, "(✿◕ ‿◕ )ノ".getBytes(UTF_8));
657+
Files.move(source, target);
658+
assertThat(Files.exists(source)).isFalse();
659+
assertThat(Files.exists(target)).isTrue();
658660
}
659661

660662
@Test
661-
public void testMove_atomicMove_notSupported() throws Exception {
663+
public void testMove_atomicCrossBucket_throwsUnsupported() throws Exception {
664+
Path source = Paths.get(URI.create("gs://military/fashion.show"));
665+
Path target = Paths.get(URI.create("gs://greenbean/adipose"));
666+
Files.write(source, "(✿◕ ‿◕ )ノ".getBytes(UTF_8));
662667
try {
663-
Path source = Paths.get(URI.create("gs://military/fashion.show"));
664-
Path target = Paths.get(URI.create("gs://greenbean/adipose"));
665-
Files.write(source, "(✿◕ ‿◕ )ノ".getBytes(UTF_8));
666668
Files.move(source, target, ATOMIC_MOVE);
667669
Assert.fail();
668670
} catch (AtomicMoveNotSupportedException ex) {
669671
assertThat(ex.getMessage()).isNotNull();
670672
}
671673
}
672674

675+
@Test
676+
public void testCreateDirectory() throws Exception {
677+
Path path = Paths.get(URI.create("gs://greenbean/dir/"));
678+
Files.createDirectory(path);
679+
assertThat(Files.exists(path)).isTrue();
680+
}
681+
673682
@Test
674683
public void testIsDirectory() throws Exception {
675684
try (FileSystem fs = FileSystems.getFileSystem(URI.create("gs://doodle"))) {

google-cloud-nio/src/test/java/com/google/cloud/storage/contrib/nio/it/ITGcsNio.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import com.google.cloud.storage.testing.RemoteStorageHelper;
4343
import com.google.cloud.testing.junit4.MultipleAttemptsRule;
4444
import com.google.common.collect.ImmutableList;
45+
import com.google.common.collect.ImmutableMap;
4546
import com.google.common.collect.Lists;
4647
import com.google.common.collect.Sets;
4748
import java.io.ByteArrayOutputStream;
@@ -1120,6 +1121,25 @@ public void testCopyWithDifferentProvider() throws IOException {
11201121
assertNotEquals(sourceFileSystem.config(), targetFileSystem.config());
11211122
}
11221123

1124+
@Test
1125+
public void testMove() throws Exception {
1126+
ImmutableMap<String, String> metadata = ImmutableMap.of("k", "v");
1127+
BlobInfo info1 = BlobInfo.newBuilder(BUCKET, "testMove-0001").setMetadata(metadata).build();
1128+
BlobInfo info2 = BlobInfo.newBuilder(BUCKET, "testMove-0002").build();
1129+
storage.create(info1, "Hello".getBytes(UTF_8), BlobTargetOption.doesNotExist());
1130+
1131+
CloudStorageFileSystem fs = getTestBucket();
1132+
CloudStoragePath src = fs.getPath(info1.getName());
1133+
CloudStoragePath dst = fs.getPath(info2.getName());
1134+
1135+
Path moved = Files.move(src, dst);
1136+
assertThat(moved).isNotNull();
1137+
1138+
BlobInfo movedInfo = storage.get(info2.getBlobId());
1139+
assertThat(movedInfo).isNotNull();
1140+
assertThat(movedInfo.getMetadata()).isEqualTo(metadata);
1141+
}
1142+
11231143
@Test
11241144
public void testListObject() throws IOException {
11251145
String firstBucket = "first-bucket-" + UUID.randomUUID().toString();

0 commit comments

Comments
 (0)