25
25
import static google .registry .request .Action .Method .POST ;
26
26
import static jakarta .servlet .http .HttpServletResponse .SC_INTERNAL_SERVER_ERROR ;
27
27
import static java .nio .charset .StandardCharsets .US_ASCII ;
28
+ import static java .nio .charset .StandardCharsets .UTF_8 ;
28
29
29
30
import com .google .cloud .storage .BlobId ;
30
- import com .google .common .base .Joiner ;
31
31
import com .google .common .collect .ImmutableList ;
32
32
import com .google .common .collect .ImmutableSet ;
33
33
import com .google .common .collect .ImmutableSortedSet ;
34
34
import com .google .common .collect .Ordering ;
35
35
import com .google .common .flogger .FluentLogger ;
36
+ import com .google .common .hash .Hasher ;
36
37
import com .google .common .hash .Hashing ;
37
- import com .google .common .io .ByteSource ;
38
38
import google .registry .bsa .api .BsaCredential ;
39
39
import google .registry .config .RegistryConfig .Config ;
40
40
import google .registry .gcs .GcsUtils ;
47
47
import google .registry .util .Clock ;
48
48
import jakarta .inject .Inject ;
49
49
import jakarta .persistence .TypedQuery ;
50
- import java .io .ByteArrayOutputStream ;
50
+ import java .io .BufferedInputStream ;
51
51
import java .io .IOException ;
52
+ import java .io .InputStream ;
52
53
import java .io .OutputStream ;
53
54
import java .io .OutputStreamWriter ;
55
+ import java .io .PipedInputStream ;
56
+ import java .io .PipedOutputStream ;
54
57
import java .io .Writer ;
55
58
import java .util .Optional ;
56
59
import java .util .zip .GZIPOutputStream ;
60
63
import okhttp3 .Request ;
61
64
import okhttp3 .RequestBody ;
62
65
import okhttp3 .Response ;
66
+ import okio .BufferedSink ;
67
+ import org .jetbrains .annotations .NotNull ;
68
+ import org .jetbrains .annotations .Nullable ;
63
69
import org .joda .time .DateTime ;
64
70
65
71
/**
66
72
* Daily action that uploads unavailable domain names on applicable TLDs to BSA.
67
73
*
68
74
* <p>The upload is a single zipped text file containing combined details for all BSA-enrolled TLDs.
69
- * The text is a newline-delimited list of punycoded fully qualified domain names, and contains all
70
- * domains on each TLD that are registered and/or reserved.
75
+ * The text is a newline-delimited list of punycoded fully qualified domain names with a trailing
76
+ * newline at the end, and contains all domains on each TLD that are registered and/or reserved.
71
77
*
72
78
* <p>The file is also uploaded to GCS to preserve it as a record for ourselves.
73
79
*/
@@ -118,7 +124,7 @@ public void run() {
118
124
// TODO(mcilwain): Implement a date Cursor, have the cronjob run frequently, and short-circuit
119
125
// the run if the daily upload is already completed.
120
126
DateTime runTime = clock .nowUtc ();
121
- String unavailableDomains = Joiner . on ( " \n " ). join ( getUnavailableDomains (runTime ) );
127
+ ImmutableSortedSet < String > unavailableDomains = getUnavailableDomains (runTime );
122
128
if (unavailableDomains .isEmpty ()) {
123
129
logger .atWarning ().log ("No unavailable domains found; terminating." );
124
130
emailSender .sendNotification (
@@ -136,12 +142,16 @@ public void run() {
136
142
}
137
143
138
144
/** Uploads the unavailable domains list to GCS in the unavailable domains bucket. */
139
- boolean uploadToGcs (String unavailableDomains , DateTime runTime ) {
145
+ boolean uploadToGcs (ImmutableSortedSet < String > unavailableDomains , DateTime runTime ) {
140
146
logger .atInfo ().log ("Uploading unavailable names file to GCS in bucket %s" , gcsBucket );
141
147
BlobId blobId = BlobId .of (gcsBucket , createFilename (runTime ));
148
+ // `gcsUtils.openOutputStream` returns a buffered stream
142
149
try (OutputStream gcsOutput = gcsUtils .openOutputStream (blobId );
143
150
Writer osWriter = new OutputStreamWriter (gcsOutput , US_ASCII )) {
144
- osWriter .write (unavailableDomains );
151
+ for (var domainName : unavailableDomains ) {
152
+ osWriter .write (domainName );
153
+ osWriter .write ("\n " );
154
+ }
145
155
return true ;
146
156
} catch (Exception e ) {
147
157
logger .atSevere ().withCause (e ).log (
@@ -150,10 +160,14 @@ boolean uploadToGcs(String unavailableDomains, DateTime runTime) {
150
160
}
151
161
}
152
162
153
- boolean uploadToBsa (String unavailableDomains , DateTime runTime ) {
163
+ boolean uploadToBsa (ImmutableSortedSet < String > unavailableDomains , DateTime runTime ) {
154
164
try {
155
- byte [] gzippedContents = gzipUnavailableDomains (unavailableDomains );
156
- String sha512Hash = ByteSource .wrap (gzippedContents ).hash (Hashing .sha512 ()).toString ();
165
+ Hasher sha512Hasher = Hashing .sha512 ().newHasher ();
166
+ unavailableDomains .stream ()
167
+ .map (name -> name + "\n " )
168
+ .forEachOrdered (line -> sha512Hasher .putString (line , UTF_8 ));
169
+ String sha512Hash = sha512Hasher .hash ().toString ();
170
+
157
171
String filename = createFilename (runTime );
158
172
OkHttpClient client = new OkHttpClient ().newBuilder ().build ();
159
173
@@ -169,7 +183,9 @@ boolean uploadToBsa(String unavailableDomains, DateTime runTime) {
169
183
.addFormDataPart (
170
184
"file" ,
171
185
String .format ("%s.gz" , filename ),
172
- RequestBody .create (gzippedContents , MediaType .parse ("application/octet-stream" )))
186
+ new StreamingRequestBody (
187
+ gzippedStream (unavailableDomains ),
188
+ MediaType .parse ("application/octet-stream" )))
173
189
.build ();
174
190
175
191
Request request =
@@ -196,15 +212,6 @@ boolean uploadToBsa(String unavailableDomains, DateTime runTime) {
196
212
}
197
213
}
198
214
199
- private byte [] gzipUnavailableDomains (String unavailableDomains ) throws IOException {
200
- try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream ()) {
201
- try (GZIPOutputStream gzipOutputStream = new GZIPOutputStream (byteArrayOutputStream )) {
202
- gzipOutputStream .write (unavailableDomains .getBytes (US_ASCII ));
203
- }
204
- return byteArrayOutputStream .toByteArray ();
205
- }
206
- }
207
-
208
215
private static String createFilename (DateTime runTime ) {
209
216
return String .format ("unavailable_domains_%s.txt" , runTime .toString ());
210
217
}
@@ -280,4 +287,65 @@ private ImmutableSortedSet<String> getUnavailableDomains(DateTime runTime) {
280
287
private static String toDomain (String domainLabel , Tld tld ) {
281
288
return String .format ("%s.%s" , domainLabel , tld .getTldStr ());
282
289
}
290
+
291
+ private InputStream gzippedStream (ImmutableSortedSet <String > unavailableDomains )
292
+ throws IOException {
293
+ PipedInputStream inputStream = new PipedInputStream ();
294
+ PipedOutputStream outputStream = new PipedOutputStream (inputStream );
295
+
296
+ new Thread (
297
+ () -> {
298
+ try {
299
+ gzipUnavailableDomains (outputStream , unavailableDomains );
300
+ } catch (Throwable e ) {
301
+ logger .atSevere ().withCause (e ).log ("Failed to gzip unavailable domains." );
302
+ try {
303
+ // This will cause the next read to throw an IOException.
304
+ inputStream .close ();
305
+ } catch (IOException ignore ) {
306
+ // Won't happen for `PipedInputStream.close()`
307
+ }
308
+ }
309
+ })
310
+ .start ();
311
+
312
+ return inputStream ;
313
+ }
314
+
315
+ private void gzipUnavailableDomains (
316
+ PipedOutputStream outputStream , ImmutableSortedSet <String > unavailableDomains )
317
+ throws IOException {
318
+ // `GZIPOutputStream` is buffered.
319
+ try (GZIPOutputStream gzipOutputStream = new GZIPOutputStream (outputStream )) {
320
+ for (String name : unavailableDomains ) {
321
+ var line = name + "\n " ;
322
+ gzipOutputStream .write (line .getBytes (US_ASCII ));
323
+ }
324
+ }
325
+ }
326
+
327
+ private static class StreamingRequestBody extends RequestBody {
328
+ private final BufferedInputStream inputStream ;
329
+ private final MediaType mediaType ;
330
+
331
+ StreamingRequestBody (InputStream inputStream , MediaType mediaType ) {
332
+ this .inputStream = new BufferedInputStream (inputStream );
333
+ this .mediaType = mediaType ;
334
+ }
335
+
336
+ @ Nullable
337
+ @ Override
338
+ public MediaType contentType () {
339
+ return mediaType ;
340
+ }
341
+
342
+ @ Override
343
+ public void writeTo (@ NotNull BufferedSink bufferedSink ) throws IOException {
344
+ byte [] buffer = new byte [2048 ];
345
+ int bytesRead ;
346
+ while ((bytesRead = inputStream .read (buffer )) != -1 ) {
347
+ bufferedSink .write (buffer , 0 , bytesRead );
348
+ }
349
+ }
350
+ }
283
351
}
0 commit comments