From 73b9ef67e4eb01530a315dea74559cb203f7535a Mon Sep 17 00:00:00 2001 From: JP Martin Date: Fri, 4 Mar 2016 09:57:03 -0800 Subject: [PATCH] Add integration test and example jar. The integration tests show that we can read/write from GCS, and should be able to run from Travis. The example jar demonstrates the other use case for NIO: adding GCS support to a legacy Java 7 program just by adding a jar file to its classpath. --- .../contrib/nio/CloudStorageFileSystem.java | 15 + .../nio/CloudStorageFileSystemProvider.java | 2 +- .../java.nio.file.spi.FileSystemProvider | 1 + .../nio/CloudStorageFileSystemTest.java | 2 + .../gcloud/storage/contrib/nio/ITGcsNio.java | 325 ++++++++++++++++++ gcloud-java-examples/README.md | 14 + gcloud-java-examples/pom.xml | 36 +- .../java/com/google/gcloud/examples/Stat.java | 99 ++++++ .../google/gcloud/spi/DefaultStorageRpc.java | 19 +- 9 files changed, 503 insertions(+), 10 deletions(-) create mode 100644 gcloud-java-contrib/gcloud-java-nio/src/main/resources/META-INF/services/java.nio.file.spi.FileSystemProvider create mode 100644 gcloud-java-contrib/gcloud-java-nio/src/test/java/com/google/gcloud/storage/contrib/nio/ITGcsNio.java create mode 100644 gcloud-java-examples/src/main/java/com/google/gcloud/examples/Stat.java diff --git a/gcloud-java-contrib/gcloud-java-nio/src/main/java/com/google/gcloud/storage/contrib/nio/CloudStorageFileSystem.java b/gcloud-java-contrib/gcloud-java-nio/src/main/java/com/google/gcloud/storage/contrib/nio/CloudStorageFileSystem.java index 88f66150e123..0f20266967e2 100644 --- a/gcloud-java-contrib/gcloud-java-nio/src/main/java/com/google/gcloud/storage/contrib/nio/CloudStorageFileSystem.java +++ b/gcloud-java-contrib/gcloud-java-nio/src/main/java/com/google/gcloud/storage/contrib/nio/CloudStorageFileSystem.java @@ -4,6 +4,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.collect.ImmutableSet; +import com.google.gcloud.storage.StorageOptions; import java.io.IOException; import java.net.URI; @@ -62,6 +63,20 @@ public static CloudStorageFileSystem forBucket(String bucket, CloudStorageConfig new CloudStorageFileSystemProvider(), bucket, checkNotNull(config)); } + /** + * Creates a new filesystem for a particular bucket, with customizable settings and storage + * options. + * + * @see #forBucket(String) + */ + public static CloudStorageFileSystem forBucket(String bucket, CloudStorageConfiguration config, + StorageOptions storageOptions) { + checkArgument(!bucket.startsWith(URI_SCHEME + ":"), + "Bucket name must not have schema: %s", bucket); + return new CloudStorageFileSystem(new CloudStorageFileSystemProvider( + checkNotNull(storageOptions)), bucket, checkNotNull(config)); + } + public static final String URI_SCHEME = "gs"; public static final String GCS_VIEW = "gcs"; public static final String BASIC_VIEW = "basic"; diff --git a/gcloud-java-contrib/gcloud-java-nio/src/main/java/com/google/gcloud/storage/contrib/nio/CloudStorageFileSystemProvider.java b/gcloud-java-contrib/gcloud-java-nio/src/main/java/com/google/gcloud/storage/contrib/nio/CloudStorageFileSystemProvider.java index 4c7acba98a96..c3da09ddf049 100644 --- a/gcloud-java-contrib/gcloud-java-nio/src/main/java/com/google/gcloud/storage/contrib/nio/CloudStorageFileSystemProvider.java +++ b/gcloud-java-contrib/gcloud-java-nio/src/main/java/com/google/gcloud/storage/contrib/nio/CloudStorageFileSystemProvider.java @@ -86,7 +86,7 @@ public CloudStorageFileSystemProvider() { this(storageOptions); } - private CloudStorageFileSystemProvider(@Nullable StorageOptions gcsStorageOptions) { + CloudStorageFileSystemProvider(@Nullable StorageOptions gcsStorageOptions) { if (gcsStorageOptions == null) { this.storage = StorageOptions.defaultInstance().service(); } else { diff --git a/gcloud-java-contrib/gcloud-java-nio/src/main/resources/META-INF/services/java.nio.file.spi.FileSystemProvider b/gcloud-java-contrib/gcloud-java-nio/src/main/resources/META-INF/services/java.nio.file.spi.FileSystemProvider new file mode 100644 index 000000000000..0b07e74a42a6 --- /dev/null +++ b/gcloud-java-contrib/gcloud-java-nio/src/main/resources/META-INF/services/java.nio.file.spi.FileSystemProvider @@ -0,0 +1 @@ +com.google.gcloud.storage.contrib.nio.CloudStorageFileSystemProvider diff --git a/gcloud-java-contrib/gcloud-java-nio/src/test/java/com/google/gcloud/storage/contrib/nio/CloudStorageFileSystemTest.java b/gcloud-java-contrib/gcloud-java-nio/src/test/java/com/google/gcloud/storage/contrib/nio/CloudStorageFileSystemTest.java index 3ef113e14744..0ef235af558f 100644 --- a/gcloud-java-contrib/gcloud-java-nio/src/test/java/com/google/gcloud/storage/contrib/nio/CloudStorageFileSystemTest.java +++ b/gcloud-java-contrib/gcloud-java-nio/src/test/java/com/google/gcloud/storage/contrib/nio/CloudStorageFileSystemTest.java @@ -5,6 +5,7 @@ import com.google.common.testing.EqualsTester; import com.google.common.testing.NullPointerTester; +import com.google.gcloud.storage.StorageOptions; import com.google.gcloud.storage.testing.LocalGcsHelper; import org.junit.Before; @@ -112,6 +113,7 @@ public void testNullness() throws IOException, NoSuchMethodException, SecurityEx new NullPointerTester() .ignore(CloudStorageFileSystem.class.getMethod("equals", Object.class)) .setDefault(CloudStorageConfiguration.class, CloudStorageConfiguration.DEFAULT); + .setDefault(StorageOptions.class, StorageOptions.defaultInstance()); tester.testAllPublicStaticMethods(CloudStorageFileSystem.class); tester.testAllPublicInstanceMethods(fs); } diff --git a/gcloud-java-contrib/gcloud-java-nio/src/test/java/com/google/gcloud/storage/contrib/nio/ITGcsNio.java b/gcloud-java-contrib/gcloud-java-nio/src/test/java/com/google/gcloud/storage/contrib/nio/ITGcsNio.java new file mode 100644 index 000000000000..e68e960b3585 --- /dev/null +++ b/gcloud-java-contrib/gcloud-java-nio/src/test/java/com/google/gcloud/storage/contrib/nio/ITGcsNio.java @@ -0,0 +1,325 @@ +package com.google.gcloud.storage.contrib.nio; + +import static com.google.common.truth.Truth.assertThat; +import static java.nio.charset.StandardCharsets.UTF_8; + +import com.google.common.collect.ImmutableList; +import com.google.gcloud.AuthCredentials; +import com.google.gcloud.storage.StorageOptions; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.StringBufferInputStream; +import java.io.StringReader; +import java.net.URI; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.CopyOption; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.List; +import java.util.Random; + + +/** + * Integration test for gcloud-nio. This test actually talks to GCS (you need an account). + * Tests both reading and writing. + */ +@RunWith(JUnit4.class) +public class ITGcsNio { + + // + // The variables below are filled in via environment variables + // (of the same name). + // You *must* set these environment variables before running this test or it will fail. + // The test also relies on you using the gcloud command-line utility to log into + // an account that has read access to the bucket you'll point the tests to. + // + // The instructions for how to get the Service Account JSON Key are + // at https://cloud.google.com/storage/docs/authentication?hl=en#service_accounts + // + // The short version is this: go to cloud.google.com/console, + // select your project, search for "API manager", click "Credentials", + // click "create credentials/service account key", new service account, + // JSON. The contents of the file that's sent to your browsers is your + // "Service Account JSON Key". + // + + // The bucket we'll read the files from (e.g. "abucket") + private final String TEST_NIO_BUCKET; + // A first file we can read (can be small) (e.g. "folder/file.txt") + private final String TEST_NIO_SML; + // The size of the small file, in bytes + private final int TEST_NIO_SML_SIZE; + // A second file we can read (can be large) + private final String TEST_NIO_LGE; + // A non-existant file we can write to. We're going to add a suffix to it + // just in case multiple tests are running concurrently. + private final String TEST_NIO_WRITE_PREFIX; + // The project that the bucket belongs to. + // This ought to match the project_id entry in the service account json key below. + private final String TEST_NIO_PROJECT; + // the full text of the service account key. + // e.g. {"type":"service_account","project_id":... + // Naturally you don't want to share this since it gives people access to this project. + private final String TEST_NIO_SERVICE_ACCOUNT_JSON_KEY; + + private final Random rnd; + + private static final List FILE_CONTENTS = ImmutableList.of( + "Tous les êtres humains naissent libres et égaux en dignité et en droits.", + "Ils sont doués de raison et de conscience et doivent agir ", + "les uns envers les autres dans un esprit de fraternité."); + + + + /** + * Sets up the test according to the provided env. vars. + */ + public ITGcsNio() { + TEST_NIO_BUCKET = getEnv("TEST_NIO_BUCKET"); + TEST_NIO_SML = getEnv("TEST_NIO_SML"); + TEST_NIO_SML_SIZE = Integer.parseInt(getEnv("TEST_NIO_SML_SIZE")); + TEST_NIO_LGE = getEnv("TEST_NIO_LGE"); + TEST_NIO_WRITE_PREFIX = getEnv("TEST_NIO_WRITE_PREFIX"); + TEST_NIO_PROJECT = getEnv("TEST_NIO_PROJECT"); + TEST_NIO_SERVICE_ACCOUNT_JSON_KEY = getEnv("TEST_NIO_SERVICE_ACCOUNT_JSON_KEY"); + rnd = new Random(); + } + + @Test + public void testFileExists() throws IOException { + CloudStorageFileSystem testBucket = getTestBucket(); + Path path = testBucket.getPath(TEST_NIO_SML); + assertThat(Files.exists(path)).isTrue(); + } + + @Test + public void testFileSize() throws IOException { + CloudStorageFileSystem testBucket = getTestBucket(); + Path path = testBucket.getPath(TEST_NIO_SML); + assertThat(Files.size(path)).isEqualTo(TEST_NIO_SML_SIZE); + } + + @Test + public void testReadByteChannel() throws IOException { + CloudStorageFileSystem testBucket = getTestBucket(); + Path path = testBucket.getPath(TEST_NIO_SML); + long size = Files.size(path); + SeekableByteChannel chan = Files.newByteChannel(path, StandardOpenOption.READ); + assertThat(chan.size()).isEqualTo(size); + ByteBuffer buf = ByteBuffer.allocate(1024 * 1024); + int timeout = 1024 * 1024; + int read = 0; + while (chan.isOpen()) { + if (timeout-- <= 0) { + break; + } + int rc = chan.read(buf); + if (rc < 0) { + // EOF + break; + } + buf.clear(); + assertThat(rc).isGreaterThan(0); + read += rc; + assertThat(chan.position() == read); + } + assertThat(read).isEqualTo(size); + } + + @Test + public void testSeek() throws IOException { + CloudStorageFileSystem testBucket = getTestBucket(); + Path path = testBucket.getPath(TEST_NIO_LGE); + long size = Files.size(path); + final ByteBuffer buf1a = ByteBuffer.allocate(100); + final ByteBuffer buf1b = ByteBuffer.allocate(100); + final ByteBuffer buf2a = ByteBuffer.allocate(100); + final ByteBuffer buf2b = ByteBuffer.allocate(100); + SeekableByteChannel chan = Files.newByteChannel(path, StandardOpenOption.READ); + assertThat(chan.size()).isEqualTo(size); + reallyRead(chan, buf1a); + long dest = size / 2; + chan.position(dest); + reallyRead(chan, buf2a); + // now go back and read it again + // (we do 2 locations because 0 is sometimes a special case). + chan.position(0); + reallyRead(chan, buf1b); + chan.position(dest); + reallyRead(chan, buf2b); + // if the two spots in the file have the same contents, then this isn't a good file for this + // test. + assertThat(buf1a.array()).isNotEqualTo(buf2a.array()); + assertThat(buf1a.array()).isEqualTo(buf1b.array()); + assertThat(buf2a.array()).isEqualTo(buf2b.array()); + } + + @Test + public void testCreate() throws IOException { + CloudStorageFileSystem testBucket = getTestBucket(); + Path path = testBucket.getPath(TEST_NIO_WRITE_PREFIX + randomSuffix()); + try { + // file shouldn't exist initially. If it does it's either because it's a leftover + // from a previous run (so we should delete the file) + // or because we're misconfigured and pointing to an actually important file + // (so we should absolutely not delete it). + // So if the file's here, don't try to fix it automatically, let the user deal with it. + assertThat(Files.exists(path)).isFalse(); + + Files.createFile(path); + // now it does, and it has size 0. + assertThat(Files.exists(path)).isTrue(); + long size = Files.size(path); + assertThat(size).isEqualTo(0); + } finally { + // let's not leave files around + Files.deleteIfExists(path); + } + } + + @Test + public void testWrite() throws IOException { + CloudStorageFileSystem testBucket = getTestBucket(); + Path path = testBucket.getPath(TEST_NIO_WRITE_PREFIX + randomSuffix()); + try { + // file shouldn't exist initially. If it does it's either because it's a leftover + // from a previous run (so we should delete the file) + // or because we're misconfigured and pointing to an actually important file + // (so we should absolutely not delete it). + // So if the file's here, don't try to fix it automatically, let the user deal with it. + assertThat(Files.exists(path)).isFalse(); + + Files.write(path, FILE_CONTENTS, UTF_8); + // now it does. + assertThat(Files.exists(path)).isTrue(); + + // let's check that the contents look OK. + ByteBuffer buf = ByteBuffer.allocate(100); + SeekableByteChannel chan = Files.newByteChannel(path, StandardOpenOption.READ); + reallyRead(chan, buf); + byte[] gotBytes = buf.array(); + byte[] wantBytes = FILE_CONTENTS.get(0).getBytes(UTF_8); + for (int i = 0; i < wantBytes.length; i++) { + assertThat(gotBytes[i] == wantBytes[i]).isTrue(); + } + + } finally { + // let's not leave files around + Files.deleteIfExists(path); + } + } + + @Test + public void testWriteOnClose() throws Exception { + CloudStorageFileSystem testBucket = getTestBucket(); + Path path = testBucket.getPath(TEST_NIO_WRITE_PREFIX + randomSuffix()); + // file shouldn't exist initially (see above) + assertThat(Files.exists(path)).isFalse(); + try { + try (SeekableByteChannel chan = Files.newByteChannel(path, StandardOpenOption.WRITE)) { + // writing lots of contents to defeat channel-internal buffering. + for (int i=0; i<9999; i++) { + for (String s : FILE_CONTENTS) { + chan.write(ByteBuffer.wrap(s.getBytes(UTF_8))); + } + } + try { + Files.size(path); + // we shouldn't make it to this line. Not using thrown.expect because + // I still want to run a few lines after the exception. + assertThat(false).isTrue(); + } catch (NoSuchFileException nsf) { + // that's what we wanted, we're good. + } + } + // channel now closed, the file should be there and with the new contents. + assertThat(Files.exists(path)).isTrue(); + assertThat(Files.size(path)).isGreaterThan(0L); + } finally { + Files.deleteIfExists(path); + } + } + + @Test + public void testCopy() throws IOException { + CloudStorageFileSystem testBucket = getTestBucket(); + Path src = testBucket.getPath(TEST_NIO_SML); + Path dst = testBucket.getPath(TEST_NIO_WRITE_PREFIX + randomSuffix()); + try { + // file shouldn't exist initially (see above). + assertThat(Files.exists(dst)).isFalse(); + + Files.copy(src, dst); + + assertThat(Files.exists(dst)).isTrue(); + assertThat(Files.size(dst)).isEqualTo(TEST_NIO_SML_SIZE); + } finally { + // let's not leave files around + Files.deleteIfExists(dst); + } + } + + private String getEnv(String name) { + String ret = System.getenv(name); + // we don't want empty strings either because then tests could access places not expected by + // the caller. + if (null == ret || ret.length() == 0) { + throw new RuntimeException(name + " environment variable must be set. Please see the " + + "instructions for setting up nio's integration tests."); + } + return ret; + } + + private int reallyRead(ReadableByteChannel chan, ByteBuffer buf) throws IOException { + int sofar = 0; + int bytes = buf.remaining(); + while (sofar < bytes) { + int read = chan.read(buf); + if (read < 0) { + throw new EOFException("channel EOF"); + } + sofar += read; + } + return sofar; + } + + private String randomSuffix() { + return "-" + rnd.nextInt(99999); + } + + + private CloudStorageFileSystem getTestBucket() throws IOException { + // in typical usage we use the single-argument version of forBucket + // and rely on the user being logged into their project with the + // gcloud tool, and then everything authenticates automagically + // (or we just use paths that start with "gs://" and rely on NIO's magic). + // + // However for the tests we want to be able to run in automated environments + // where we can set environment variables but not necessarily install gcloud + // or run it. That's why we're setting the credentials programmatically. + StorageOptions storageOptions = StorageOptions.builder() + .authCredentials(AuthCredentials.createForJson(new ByteArrayInputStream + (TEST_NIO_SERVICE_ACCOUNT_JSON_KEY.getBytes(UTF_8)))) + .projectId(TEST_NIO_PROJECT) + .build(); + return CloudStorageFileSystem.forBucket( + TEST_NIO_BUCKET, CloudStorageConfiguration.DEFAULT, storageOptions); + } + +} diff --git a/gcloud-java-examples/README.md b/gcloud-java-examples/README.md index 8030d14d09e7..64aa4eea4e52 100644 --- a/gcloud-java-examples/README.md +++ b/gcloud-java-examples/README.md @@ -87,6 +87,20 @@ To run examples from your command line: mvn exec:java -Dexec.mainClass="com.google.gcloud.examples.StorageExample" -Dexec.args="download test.txt" mvn exec:java -Dexec.mainClass="com.google.gcloud.examples.StorageExample" -Dexec.args="delete test.txt" ``` + * Here's an example run of `Stat`. + + Before running the example, go to the [Google Developers Console][developers-console] to ensure that Google Cloud Storage API is enabled and that you have a bucket with a file in it. + Compile the JAR with: + ``` + mvn package -DskipTests + ``` + Then run the sample with: + ``` + java -cp gcloud-java-contrib/gcloud-java-nio/target/gcloud-java-nio-0.1.4-SNAPSHOT.jar:gcloud-java-examples/target/gcloud-java-examples-0.1.4-SNAPSHOT-jar-with-dependencies.jar com.google.gcloud.examples.Stat --check + ``` + The sample doesn't have anything about GCS in it: it gets that ability from the nio jar that + we're adding to the classpath. When you do this in practice you also have to add all of its + dependencies (the gcloud jar and its own dependencies). Troubleshooting --------------- diff --git a/gcloud-java-examples/pom.xml b/gcloud-java-examples/pom.xml index 5597f1f44132..531066821ca7 100644 --- a/gcloud-java-examples/pom.xml +++ b/gcloud-java-examples/pom.xml @@ -22,6 +22,12 @@ gcloud-java ${project.version} + + org.apache.maven.plugins + maven-assembly-plugin + 2.5.4 + + @@ -32,6 +38,34 @@ false - + + + org.apache.maven.plugins + maven-assembly-plugin + 2.5.4 + + + + jar-with-dependencies + + + + + com.google.gcloud.examples.Stat + + + + + + make-assembly + + package + + single + + + + + diff --git a/gcloud-java-examples/src/main/java/com/google/gcloud/examples/Stat.java b/gcloud-java-examples/src/main/java/com/google/gcloud/examples/Stat.java new file mode 100644 index 000000000000..736e30b36f11 --- /dev/null +++ b/gcloud-java-examples/src/main/java/com/google/gcloud/examples/Stat.java @@ -0,0 +1,99 @@ +package com.google.gcloud.examples; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.spi.FileSystemProvider; + +/** + * Stat is a super-simple program that just displays the size of the file + * passed as argument. + * + *

It's meant to be used to test GCloud's integration with Java NIO. + * + *

Set up a Google Cloud Storage bucket, log in with gcloud, and try it. + */ +public class Stat { + + /** + * See the class documentation. + */ + public static void main(String[] args) throws IOException { + if (args.length == 0) { + System.out.println("Use --help for help or provide a file name."); + return; + } + if (args[0].equals("--help")) { + help(); + return; + } + if (args[0].equals("--list")) { + listFilesystems(); + return; + } + if (args[0].equals("--check")) { + checkGcs(); + return; + } + for (String a : args) { + statFile(a); + } + } + + /** + * Print the length of the indicated file. + * + *

This uses the normal Java NIO Api, so it can take advantage of any installed + * NIO Filesystem provider without any extra effort. + */ + private static void statFile(String fname) { + try { + Path path = Paths.get(new URI(fname)); + long size = Files.size(path); + System.out.println(fname + ": " + size + " bytes."); + } catch (Exception ex) { + System.out.println(fname + ": " + ex.toString()); + } + } + + private static void help() { + String[] help = + {"Usage: java -jar gcloud-java-examples-X-Y-Z.jar ", + "to display the length of that file.", + "", + "or: java -jar gcloud-java-examples-X-Y-Z.jar --list", + "to list the filesystem providers.", + "", + "The purpose of this tool is to demonstrate the gcloud NIO filesystem provider.", + "", + "This tool normally knows nothing of Google Cloud Storage. If you pass it a gs://", + "file name, it will show an error.", + "However, by just adding the gcloud-nio jar in your classpath, this tool is made", + "aware of gs:// paths and can access files on the cloud.", + "", + "The gcloud NIO filesystem provider can similarly enable existing Java 7 programs", + "to read and write cloud files, even if they have no special built-in cloud support." + }; + for (String s : help) { + System.out.println(s); + } + } + + private static void listFilesystems() { + System.out.println("Installed filesystem providers:"); + for (FileSystemProvider p : FileSystemProvider.installedProviders()) { + System.out.println(" " + p.getScheme()); + } + } + + private static void checkGcs() { + FileSystem fs = FileSystems.getFileSystem(URI.create("gs://domain-registry-alpha")); + System.out.println("We seem to be able to instantiate a gs:// filesystem."); + System.out.println("isOpen: " + fs.isOpen()); + System.out.println("isReadOnly: " + fs.isReadOnly()); + } +} diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/spi/DefaultStorageRpc.java b/gcloud-java-storage/src/main/java/com/google/gcloud/spi/DefaultStorageRpc.java index dc84a1de5559..eff3b1929d86 100644 --- a/gcloud-java-storage/src/main/java/com/google/gcloud/spi/DefaultStorageRpc.java +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/spi/DefaultStorageRpc.java @@ -465,15 +465,18 @@ public void write(String uploadId, byte[] toWrite, int toWriteOffset, long destO GenericUrl url = new GenericUrl(uploadId); HttpRequest httpRequest = storage.getRequestFactory().buildPutRequest(url, new ByteArrayContent(null, toWrite, toWriteOffset, length)); - long limit = destOffset + length; - StringBuilder range = new StringBuilder("bytes "); - range.append(destOffset).append('-').append(limit - 1).append('/'); - if (last) { - range.append(limit); - } else { - range.append('*'); + // 0-length requests are valid, don't need content-range. + if (length > 0) { + long limit = destOffset + length; + StringBuilder range = new StringBuilder("bytes "); + range.append(destOffset).append('-').append(limit - 1).append('/'); + if (last) { + range.append(limit); + } else { + range.append('*'); + } + httpRequest.getHeaders().setContentRange(range.toString()); } - httpRequest.getHeaders().setContentRange(range.toString()); int code; String message; IOException exception = null;