Skip to content

Commit b6d45df

Browse files
jean-philippe-martinyihanzhen
authored andcommitted
Requester-Pays bucket support. (#3406)
* Requester-Pays bucket support. Code and integration test. To use this feature, set the "userProject" setting in the CloudStorageConfiguration. Optionally, set autoDetectRequesterPays to avoid automatically unset userProject if the bucket isn't requester-pays. * linter fixes * minor linter fixes * reviewer comments * apply all codacy recommendations * Put defaults back, remove unused import.
1 parent 71637ce commit b6d45df

File tree

6 files changed

+411
-43
lines changed

6 files changed

+411
-43
lines changed

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

+46-1
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
package com.google.cloud.storage.contrib.nio;
1818

1919
import static com.google.common.base.Preconditions.checkArgument;
20+
import static com.google.common.base.Preconditions.checkNotNull;
2021

2122
import com.google.auto.value.AutoValue;
2223

24+
import javax.annotation.Nullable;
2325
import java.util.Map;
2426

2527
/**
@@ -65,6 +67,25 @@ public abstract class CloudStorageConfiguration {
6567
*/
6668
public abstract int maxChannelReopens();
6769

70+
/**
71+
* Returns the project to be billed when accessing buckets. Leave empty for normal semantics,
72+
* set to bill that project (project you own) for all accesses. This is required for accessing
73+
* requester-pays buckets. This value cannot be null.
74+
*/
75+
public abstract @Nullable String userProject();
76+
77+
/**
78+
* Returns whether userProject will be cleared for non-requester-pays buckets. That is,
79+
* if false (the default value), setting userProject causes that project to be billed
80+
* regardless of whether the bucket is requester-pays or not. If true, setting
81+
* userProject will only cause that project to be billed when the project is requester-pays.
82+
*
83+
* Setting this will cause the bucket to be accessed when the CloudStorageFileSystem object
84+
* is created.
85+
*/
86+
public abstract boolean useUserProjectOnlyForRequesterPaysBuckets();
87+
88+
6889
/**
6990
* Creates a new builder, initialized with the following settings:
7091
*
@@ -90,6 +111,9 @@ public static final class Builder {
90111
private boolean usePseudoDirectories = true;
91112
private int blockSize = CloudStorageFileSystem.BLOCK_SIZE_DEFAULT;
92113
private int maxChannelReopens = 0;
114+
private @Nullable String userProject = null;
115+
// This of this as "clear userProject if not RequesterPays"
116+
private boolean useUserProjectOnlyForRequesterPaysBuckets = false;
93117

94118
/**
95119
* Changes current working directory for new filesystem. This defaults to the root directory.
@@ -99,6 +123,7 @@ public static final class Builder {
99123
* @throws IllegalArgumentException if {@code path} is not absolute.
100124
*/
101125
public Builder workingDirectory(String path) {
126+
checkNotNull(path);
102127
checkArgument(UnixPath.getPath(false, path).isAbsolute(), "not absolute: %s", path);
103128
workingDirectory = path;
104129
return this;
@@ -147,6 +172,16 @@ public Builder maxChannelReopens(int value) {
147172
return this;
148173
}
149174

175+
public Builder userProject(String value) {
176+
userProject = value;
177+
return this;
178+
}
179+
180+
public Builder autoDetectRequesterPays(boolean value) {
181+
useUserProjectOnlyForRequesterPaysBuckets = value;
182+
return this;
183+
}
184+
150185
/**
151186
* Creates new instance without destroying builder.
152187
*/
@@ -157,7 +192,9 @@ public CloudStorageConfiguration build() {
157192
stripPrefixSlash,
158193
usePseudoDirectories,
159194
blockSize,
160-
maxChannelReopens);
195+
maxChannelReopens,
196+
userProject,
197+
useUserProjectOnlyForRequesterPaysBuckets);
161198
}
162199

163200
Builder(CloudStorageConfiguration toModify) {
@@ -167,6 +204,8 @@ public CloudStorageConfiguration build() {
167204
usePseudoDirectories = toModify.usePseudoDirectories();
168205
blockSize = toModify.blockSize();
169206
maxChannelReopens = toModify.maxChannelReopens();
207+
userProject = toModify.userProject();
208+
useUserProjectOnlyForRequesterPaysBuckets = toModify.useUserProjectOnlyForRequesterPaysBuckets();
170209
}
171210

172211
Builder() {}
@@ -201,6 +240,12 @@ static private CloudStorageConfiguration fromMap(Builder builder, Map<String, ?>
201240
case "maxChannelReopens":
202241
builder.maxChannelReopens((Integer) entry.getValue());
203242
break;
243+
case "userProject":
244+
builder.userProject((String) entry.getValue());
245+
break;
246+
case "useUserProjectOnlyForRequesterPaysBuckets":
247+
builder.autoDetectRequesterPays((Boolean) entry.getValue());
248+
break;
204249
default:
205250
throw new IllegalArgumentException(entry.getKey());
206251
}

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

+20-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static com.google.common.base.Preconditions.checkNotNull;
2121

2222
import com.google.cloud.storage.StorageOptions;
23+
import com.google.common.base.Strings;
2324
import com.google.common.collect.ImmutableSet;
2425

2526
import java.io.IOException;
@@ -33,6 +34,7 @@
3334
import java.nio.file.WatchService;
3435
import java.nio.file.attribute.FileTime;
3536
import java.nio.file.attribute.UserPrincipalLookupService;
37+
import java.util.HashMap;
3638
import java.util.Objects;
3739
import java.util.Set;
3840

@@ -112,8 +114,9 @@ public static CloudStorageFileSystem forBucket(String bucket) {
112114
public static CloudStorageFileSystem forBucket(String bucket, CloudStorageConfiguration config) {
113115
checkArgument(
114116
!bucket.startsWith(URI_SCHEME + ":"), "Bucket name must not have schema: %s", bucket);
117+
checkNotNull(config);
115118
return new CloudStorageFileSystem(
116-
new CloudStorageFileSystemProvider(), bucket, checkNotNull(config));
119+
new CloudStorageFileSystemProvider(config.userProject()), bucket, config);
117120
}
118121

119122
/**
@@ -136,15 +139,29 @@ public static CloudStorageFileSystem forBucket(String bucket, CloudStorageConfig
136139
@Nullable StorageOptions storageOptions) {
137140
checkArgument(!bucket.startsWith(URI_SCHEME + ":"),
138141
"Bucket name must not have schema: %s", bucket);
139-
return new CloudStorageFileSystem(new CloudStorageFileSystemProvider(storageOptions),
142+
return new CloudStorageFileSystem(new CloudStorageFileSystemProvider(config.userProject(), storageOptions),
140143
bucket, checkNotNull(config));
141144
}
142145

143146
CloudStorageFileSystem(
144147
CloudStorageFileSystemProvider provider, String bucket, CloudStorageConfiguration config) {
145148
checkArgument(!bucket.isEmpty(), "bucket");
146-
this.provider = provider;
147149
this.bucket = bucket;
150+
if (config.useUserProjectOnlyForRequesterPaysBuckets()) {
151+
if (Strings.isNullOrEmpty(config.userProject())) {
152+
throw new IllegalArgumentException("If useUserProjectOnlyForRequesterPaysBuckets is set, then userProject must be set too.");
153+
}
154+
// detect whether we want to pay for these accesses or not.
155+
if (!provider.requesterPays(bucket)) {
156+
// update config (just to ease debugging, we're not actually using config.userProject later.
157+
HashMap<String, String> disableUserProject = new HashMap<>();
158+
disableUserProject.put("userProject", "");
159+
config = CloudStorageConfiguration.fromMap(config, disableUserProject);
160+
// update the provider (this is the most important bit)
161+
provider = provider.withNoUserProject();
162+
}
163+
}
164+
this.provider = provider;
148165
this.config = config;
149166
}
150167

0 commit comments

Comments
 (0)