2525import static google .registry .request .Action .Method .POST ;
2626import static jakarta .servlet .http .HttpServletResponse .SC_INTERNAL_SERVER_ERROR ;
2727import static java .nio .charset .StandardCharsets .US_ASCII ;
28+ import static java .nio .charset .StandardCharsets .UTF_8 ;
2829
2930import com .google .cloud .storage .BlobId ;
30- import com .google .common .base .Joiner ;
3131import com .google .common .collect .ImmutableList ;
3232import com .google .common .collect .ImmutableSet ;
3333import com .google .common .collect .ImmutableSortedSet ;
3434import com .google .common .collect .Ordering ;
3535import com .google .common .flogger .FluentLogger ;
36+ import com .google .common .hash .Hasher ;
3637import com .google .common .hash .Hashing ;
37- import com .google .common .io .ByteSource ;
3838import google .registry .bsa .api .BsaCredential ;
3939import google .registry .config .RegistryConfig .Config ;
4040import google .registry .gcs .GcsUtils ;
4747import google .registry .util .Clock ;
4848import jakarta .inject .Inject ;
4949import jakarta .persistence .TypedQuery ;
50- import java .io .ByteArrayOutputStream ;
50+ import java .io .BufferedInputStream ;
5151import java .io .IOException ;
52+ import java .io .InputStream ;
5253import java .io .OutputStream ;
5354import java .io .OutputStreamWriter ;
55+ import java .io .PipedInputStream ;
56+ import java .io .PipedOutputStream ;
5457import java .io .Writer ;
5558import java .util .Optional ;
5659import java .util .zip .GZIPOutputStream ;
6063import okhttp3 .Request ;
6164import okhttp3 .RequestBody ;
6265import okhttp3 .Response ;
66+ import okio .BufferedSink ;
67+ import org .jetbrains .annotations .NotNull ;
68+ import org .jetbrains .annotations .Nullable ;
6369import org .joda .time .DateTime ;
6470
6571/**
6672 * Daily action that uploads unavailable domain names on applicable TLDs to BSA.
6773 *
6874 * <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.
7177 *
7278 * <p>The file is also uploaded to GCS to preserve it as a record for ourselves.
7379 */
@@ -118,7 +124,7 @@ public void run() {
118124 // TODO(mcilwain): Implement a date Cursor, have the cronjob run frequently, and short-circuit
119125 // the run if the daily upload is already completed.
120126 DateTime runTime = clock .nowUtc ();
121- String unavailableDomains = Joiner . on ( " \n " ). join ( getUnavailableDomains (runTime ) );
127+ ImmutableSortedSet < String > unavailableDomains = getUnavailableDomains (runTime );
122128 if (unavailableDomains .isEmpty ()) {
123129 logger .atWarning ().log ("No unavailable domains found; terminating." );
124130 emailSender .sendNotification (
@@ -136,12 +142,16 @@ public void run() {
136142 }
137143
138144 /** 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 ) {
140146 logger .atInfo ().log ("Uploading unavailable names file to GCS in bucket %s" , gcsBucket );
141147 BlobId blobId = BlobId .of (gcsBucket , createFilename (runTime ));
148+ // `gcsUtils.openOutputStream` returns a buffered stream
142149 try (OutputStream gcsOutput = gcsUtils .openOutputStream (blobId );
143150 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+ }
145155 return true ;
146156 } catch (Exception e ) {
147157 logger .atSevere ().withCause (e ).log (
@@ -150,10 +160,14 @@ boolean uploadToGcs(String unavailableDomains, DateTime runTime) {
150160 }
151161 }
152162
153- boolean uploadToBsa (String unavailableDomains , DateTime runTime ) {
163+ boolean uploadToBsa (ImmutableSortedSet < String > unavailableDomains , DateTime runTime ) {
154164 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+
157171 String filename = createFilename (runTime );
158172 OkHttpClient client = new OkHttpClient ().newBuilder ().build ();
159173
@@ -169,7 +183,9 @@ boolean uploadToBsa(String unavailableDomains, DateTime runTime) {
169183 .addFormDataPart (
170184 "file" ,
171185 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" )))
173189 .build ();
174190
175191 Request request =
@@ -196,15 +212,6 @@ boolean uploadToBsa(String unavailableDomains, DateTime runTime) {
196212 }
197213 }
198214
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-
208215 private static String createFilename (DateTime runTime ) {
209216 return String .format ("unavailable_domains_%s.txt" , runTime .toString ());
210217 }
@@ -280,4 +287,65 @@ private ImmutableSortedSet<String> getUnavailableDomains(DateTime runTime) {
280287 private static String toDomain (String domainLabel , Tld tld ) {
281288 return String .format ("%s.%s" , domainLabel , tld .getTldStr ());
282289 }
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+ }
283351}
0 commit comments