|
23 | 23 | import java.io.File; |
24 | 24 | import java.io.FileInputStream; |
25 | 25 | import java.io.FileOutputStream; |
| 26 | +import java.io.IOException; |
26 | 27 | import java.io.InputStream; |
27 | 28 | import java.io.InputStreamReader; |
28 | 29 | import java.io.OutputStream; |
@@ -384,29 +385,29 @@ public void uploadFile(String httpMethod, String urlString, String localFileName |
384 | 385 | conn.setRequestProperty("content-type", contentType); |
385 | 386 | conn.setRequestProperty("content-length", Long.toString(new File(localFileName).length())); |
386 | 387 | connectWithRetry(conn); |
387 | | - OutputStream out = null; |
388 | | - InputStream in = null; |
389 | | - BufferedReader br = null; |
390 | 388 |
|
391 | 389 | try { |
392 | | - out = conn.getOutputStream(); |
393 | | - in = new FileInputStream(localFileName); |
| 390 | + writeFileToConnection(conn, httpMethod, urlString, localFileName); |
| 391 | + checkUploadResponse(conn, httpMethod, urlString, localFileName); |
| 392 | + } finally { |
| 393 | + conn.disconnect(); |
| 394 | + } |
| 395 | + } |
| 396 | + |
| 397 | + private void writeFileToConnection(HttpURLConnection conn, String httpMethod, String urlString, String localFileName) throws IOException { |
| 398 | + long bytesWritten = 0; |
| 399 | + try (OutputStream out = conn.getOutputStream(); |
| 400 | + InputStream in = new FileInputStream(localFileName)) { |
394 | 401 | byte[] buf = new byte[ChunkSize]; |
395 | | - int len = 0; |
| 402 | + int len; |
396 | 403 | while ((len = in.read(buf)) > 0) { |
397 | 404 | out.write(buf, 0, len); |
| 405 | + bytesWritten += len; |
398 | 406 | } |
399 | 407 | out.flush(); |
400 | | - } finally { |
401 | | - if (in != null) |
402 | | - in.close(); |
403 | | - |
404 | | - if (out != null) |
405 | | - out.close(); |
406 | | - |
407 | | - if (br != null) |
408 | | - br.close(); |
409 | | - conn.disconnect(); |
| 408 | + } catch (IOException e) { |
| 409 | + throw new IOException(String.format("Upload of %s to %s %s failed after writing %d of %d bytes: %s", |
| 410 | + localFileName, httpMethod, urlString, bytesWritten, new File(localFileName).length(), e.getMessage()), e); |
410 | 411 | } |
411 | 412 | } |
412 | 413 |
|
@@ -442,32 +443,82 @@ public void uploadVmdkFile(String httpMethod, String urlString, String localFile |
442 | 443 | conn.setRequestProperty("content-length", Long.toString(new File(localFileName).length())); |
443 | 444 | connectWithRetry(conn); |
444 | 445 |
|
445 | | - BufferedOutputStream bos = null; |
446 | | - BufferedInputStream is = null; |
447 | 446 | try { |
448 | | - bos = new BufferedOutputStream(conn.getOutputStream()); |
449 | | - is = new BufferedInputStream(new FileInputStream(localFileName)); |
450 | | - int bufferSize = ChunkSize; |
451 | | - byte[] buffer = new byte[bufferSize]; |
| 447 | + writeVmdkFileToConnection(conn, httpMethod, urlString, localFileName, totalBytesUpdated, progressUpdater); |
| 448 | + checkUploadResponse(conn, httpMethod, urlString, localFileName); |
| 449 | + } finally { |
| 450 | + conn.disconnect(); |
| 451 | + } |
| 452 | + } |
| 453 | + |
| 454 | + private void writeVmdkFileToConnection(HttpURLConnection conn, String httpMethod, String urlString, String localFileName, long totalBytesUpdated, |
| 455 | + ActionDelegate<Long> progressUpdater) throws IOException { |
| 456 | + long bytesWrittenThisCall = 0; |
| 457 | + try (BufferedOutputStream bos = new BufferedOutputStream(conn.getOutputStream()); |
| 458 | + BufferedInputStream is = new BufferedInputStream(new FileInputStream(localFileName))) { |
| 459 | + byte[] buffer = new byte[ChunkSize]; |
452 | 460 | while (true) { |
453 | | - int bytesRead = is.read(buffer, 0, bufferSize); |
| 461 | + int bytesRead = is.read(buffer, 0, ChunkSize); |
454 | 462 | if (bytesRead == -1) { |
455 | 463 | break; |
456 | 464 | } |
457 | 465 | bos.write(buffer, 0, bytesRead); |
458 | 466 | totalBytesUpdated += bytesRead; |
| 467 | + bytesWrittenThisCall += bytesRead; |
459 | 468 | bos.flush(); |
460 | 469 | if (progressUpdater != null) |
461 | 470 | progressUpdater.action(new Long(totalBytesUpdated)); |
462 | 471 | } |
463 | 472 | bos.flush(); |
464 | | - } finally { |
465 | | - if (is != null) |
466 | | - is.close(); |
467 | | - if (bos != null) |
468 | | - bos.close(); |
| 473 | + } catch (IOException e) { |
| 474 | + throw new IOException(String.format("Upload of %s to %s %s failed after writing %d of %d bytes for this file " + |
| 475 | + "(%d bytes total written so far for this import): %s", |
| 476 | + localFileName, httpMethod, urlString, bytesWrittenThisCall, new File(localFileName).length(), totalBytesUpdated, e.getMessage()), e); |
| 477 | + } |
| 478 | + } |
469 | 479 |
|
470 | | - conn.disconnect(); |
| 480 | + /** |
| 481 | + * HttpURLConnection does not surface a failed request just because the client finished writing the |
| 482 | + * request body without an IOException: with chunked transfer encoding many HTTP servers, including |
| 483 | + * ESXi's NFC endpoint, read and discard the whole body before responding with an error status (e.g. a |
| 484 | + * VMFS file lock, an out-of-space datastore, or an authentication/session failure). Silently ignoring |
| 485 | + * the response code is exactly how a rejected/short write can look like a "successful" upload to |
| 486 | + * CloudStack. Surface the actual status code, reason phrase and (if any) response body so failures like |
| 487 | + * "file locked" are visible in the CloudStack logs instead of only in vCenter/ESXi's own logs, if at all. |
| 488 | + */ |
| 489 | + private void checkUploadResponse(HttpURLConnection conn, String httpMethod, String urlString, String localFileName) throws IOException { |
| 490 | + int responseCode; |
| 491 | + try { |
| 492 | + responseCode = conn.getResponseCode(); |
| 493 | + } catch (IOException e) { |
| 494 | + throw new IOException(String.format("Unable to read the response for %s %s (uploading %s): %s", |
| 495 | + httpMethod, urlString, localFileName, e.getMessage()), e); |
| 496 | + } |
| 497 | + |
| 498 | + if (responseCode < 200 || responseCode >= 300) { |
| 499 | + String responseBody = readResponseBodyQuietly(conn); |
| 500 | + boolean hasBody = responseBody != null && !responseBody.trim().isEmpty(); |
| 501 | + throw new IOException(String.format( |
| 502 | + "%s %s rejected the upload of %s with HTTP %d %s%s", |
| 503 | + httpMethod, urlString, localFileName, responseCode, conn.getResponseMessage(), |
| 504 | + hasBody ? (": " + responseBody) : "")); |
| 505 | + } |
| 506 | + } |
| 507 | + |
| 508 | + private String readResponseBodyQuietly(HttpURLConnection conn) { |
| 509 | + InputStream errorStream = conn.getErrorStream(); |
| 510 | + if (errorStream == null) { |
| 511 | + return null; |
| 512 | + } |
| 513 | + try (BufferedReader reader = new BufferedReader(new InputStreamReader(errorStream, getCharSetFromConnection(conn)))) { |
| 514 | + StringBuilder sb = new StringBuilder(); |
| 515 | + String line; |
| 516 | + while ((line = reader.readLine()) != null && sb.length() < 1024) { |
| 517 | + sb.append(line); |
| 518 | + } |
| 519 | + return sb.toString(); |
| 520 | + } catch (IOException e) { |
| 521 | + return null; |
471 | 522 | } |
472 | 523 | } |
473 | 524 |
|
|