2727import java .io .RandomAccessFile ;
2828import java .net .URI ;
2929import java .net .URISyntaxException ;
30+ import java .util .Arrays ;
3031import java .util .Date ;
3132import java .util .List ;
3233
@@ -80,6 +81,18 @@ public class HttpTemplateDownloader extends ManagedContextRunnable implements Te
8081 private ResourceType resourceType = ResourceType .TEMPLATE ;
8182 private final HttpMethodRetryHandler myretryhandler ;
8283 private boolean followRedirects = false ;
84+ private boolean isChunkedTransfer ;
85+
86+ protected static final List <String > CUSTOM_HEADERS_FOR_CHUNKED_TRANSFER_SIZE = Arrays .asList (
87+ "x-goog-stored-content-length" ,
88+ "x-goog-meta-size" ,
89+ "x-amz-meta-size" ,
90+ "x-amz-meta-content-length" ,
91+ "x-object-meta-size" ,
92+ "x-original-content-length" ,
93+ "x-oss-meta-content-length" ,
94+ "x-file-size" );
95+ private static final long MIN_FORMAT_VERIFICATION_SIZE = 1024 * 1024 ;
8396
8497 public HttpTemplateDownloader (StorageLayer storageLayer , String downloadUrl , String toDir , DownloadCompleteCallback callback , long maxTemplateSizeInBytes ,
8598 String user , String password , Proxy proxy , ResourceType resourceType ) {
@@ -205,13 +218,11 @@ public long download(boolean resume, DownloadCompleteCallback callback) {
205218 RandomAccessFile out = new RandomAccessFile (file , "rw" );
206219 ) {
207220 out .seek (localFileSize );
208-
209- logger .info ("Starting download from " + downloadUrl + " to " + toFile + " remoteSize=" + toHumanReadableSize (remoteSize ) + " , max size=" + toHumanReadableSize (maxTemplateSizeInBytes ));
210-
211- if (copyBytes (file , in , out )) return 0 ;
212-
221+ logger .info ("Starting download from {} to {} remoteSize={} , max size={}" ,downloadUrl , toFile ,
222+ toHumanReadableSize (remoteSize ), toHumanReadableSize (maxTemplateSizeInBytes ));
223+ boolean eof = copyBytes (file , in , out );
213224 Date finish = new Date ();
214- checkDowloadCompletion ( );
225+ checkDownloadCompletion ( eof );
215226 downloadTime += finish .getTime () - start .getTime ();
216227 } finally { /* in.close() and out.close() */ }
217228 return totalBytes ;
@@ -237,28 +248,32 @@ public long download(boolean resume, DownloadCompleteCallback callback) {
237248 }
238249
239250 private boolean copyBytes (File file , InputStream in , RandomAccessFile out ) throws IOException {
240- int bytes ;
241- byte [] block = new byte [CHUNK_SIZE ];
251+ byte [] buffer = new byte [CHUNK_SIZE ];
242252 long offset = 0 ;
243- boolean done = false ;
244253 VerifyFormat verifyFormat = new VerifyFormat (file );
245254 status = Status .IN_PROGRESS ;
246- while (!done && status != Status .ABORTED && offset <= remoteSize ) {
247- if ((bytes = in .read (block , 0 , CHUNK_SIZE )) > -1 ) {
248- offset = writeBlock (bytes , out , block , offset );
249- if (!ResourceType .SNAPSHOT .equals (resourceType ) &&
250- !verifyFormat .isVerifiedFormat () &&
251- (offset >= 1048576 || offset >= remoteSize )) { //let's check format after we get 1MB or full file
252- verifyFormat .invoke ();
253- }
254- } else {
255- done = true ;
255+ while (status != Status .ABORTED ) {
256+ int bytesRead = in .read (buffer , 0 , CHUNK_SIZE );
257+ if (bytesRead == -1 ) {
258+ logger .debug ("Reached EOF on input stream" );
259+ break ;
260+ }
261+ offset = writeBlock (bytesRead , out , buffer , offset );
262+ if (!ResourceType .SNAPSHOT .equals (resourceType )
263+ && !verifyFormat .isVerifiedFormat ()
264+ && (offset >= MIN_FORMAT_VERIFICATION_SIZE || offset >= remoteSize )) {
265+ verifyFormat .invoke ();
266+ }
267+ if (offset >= remoteSize ) {
268+ logger .debug ("Reached expected remote size limit: {} bytes" , remoteSize );
269+ break ;
256270 }
257271 }
258272 out .getFD ().sync ();
259- return false ;
273+ return ! Status . ABORTED . equals ( status ) ;
260274 }
261275
276+
262277 private long writeBlock (int bytes , RandomAccessFile out , byte [] block , long offset ) throws IOException {
263278 out .write (block , 0 , bytes );
264279 offset += bytes ;
@@ -267,11 +282,13 @@ private long writeBlock(int bytes, RandomAccessFile out, byte[] block, long offs
267282 return offset ;
268283 }
269284
270- private void checkDowloadCompletion ( ) {
285+ private void checkDownloadCompletion ( boolean eof ) {
271286 String downloaded = "(incomplete download)" ;
272- if (totalBytes >= remoteSize ) {
287+ if (eof && (( totalBytes >= remoteSize ) || ( isChunkedTransfer && remoteSize == maxTemplateSizeInBytes )) ) {
273288 status = Status .DOWNLOAD_FINISHED ;
274- downloaded = "(download complete remote=" + toHumanReadableSize (remoteSize ) + " bytes)" ;
289+ downloaded = "(download complete remote=" +
290+ (remoteSize == maxTemplateSizeInBytes ? toHumanReadableSize (remoteSize ) : "unknown" ) +
291+ " bytes)" ;
275292 }
276293 errorString = "Downloaded " + toHumanReadableSize (totalBytes ) + " bytes " + downloaded ;
277294 }
@@ -293,18 +310,42 @@ private void checkAndSetDownloadSize() {
293310 }
294311 }
295312
313+ protected long getRemoteSizeForChunkedTransfer () {
314+ for (String headerKey : CUSTOM_HEADERS_FOR_CHUNKED_TRANSFER_SIZE ) {
315+ Header header = request .getResponseHeader (headerKey );
316+ if (header == null ) {
317+ continue ;
318+ }
319+ try {
320+ return Long .parseLong (header .getValue ());
321+ } catch (NumberFormatException ignored ) {}
322+ }
323+ Header contentRangeHeader = request .getResponseHeader ("Content-Range" );
324+ if (contentRangeHeader != null ) {
325+ String contentRange = contentRangeHeader .getValue ();
326+ if (contentRange != null && contentRange .contains ("/" )) {
327+ String totalSize = contentRange .substring (contentRange .indexOf ('/' ) + 1 ).trim ();
328+ return Long .parseLong (totalSize );
329+ }
330+ }
331+ return 0 ;
332+ }
333+
296334 private boolean tryAndGetRemoteSize () {
297335 Header contentLengthHeader = request .getResponseHeader ("content-length" );
298- boolean chunked = false ;
336+ isChunkedTransfer = false ;
299337 long reportedRemoteSize = 0 ;
300338 if (contentLengthHeader == null ) {
301339 Header chunkedHeader = request .getResponseHeader ("Transfer-Encoding" );
302- if (chunkedHeader == null || !"chunked" .equalsIgnoreCase (chunkedHeader .getValue ())) {
340+ if (chunkedHeader != null && "chunked" .equalsIgnoreCase (chunkedHeader .getValue ())) {
341+ isChunkedTransfer = true ;
342+ reportedRemoteSize = getRemoteSizeForChunkedTransfer ();
343+ logger .debug ("{} is using chunked transfer encoding, possible remote size: {}" , downloadUrl ,
344+ reportedRemoteSize );
345+ } else {
303346 status = Status .UNRECOVERABLE_ERROR ;
304347 errorString = " Failed to receive length of download " ;
305348 return false ;
306- } else if ("chunked" .equalsIgnoreCase (chunkedHeader .getValue ())) {
307- chunked = true ;
308349 }
309350 } else {
310351 reportedRemoteSize = Long .parseLong (contentLengthHeader .getValue ());
@@ -316,9 +357,11 @@ private boolean tryAndGetRemoteSize() {
316357 return false ;
317358 }
318359 }
319-
320360 if (remoteSize == 0 ) {
321361 remoteSize = reportedRemoteSize ;
362+ if (remoteSize != 0 ) {
363+ logger .debug ("Remote size for {} found to be {}" , downloadUrl , toHumanReadableSize (remoteSize ));
364+ }
322365 }
323366 return true ;
324367 }
0 commit comments