diff --git a/doc/files.md b/doc/files.md index b2247500c..f275b256c 100644 --- a/doc/files.md +++ b/doc/files.md @@ -29,6 +29,7 @@ file's contents, upload new versions, and perform other common file operations - [Lock a File](#lock-a-file) - [Unlock a File](#unlock-a-file) - [Find File for Shared Link](#find-file-for-shared-link) +- [Download File for Shared Link](#download-file-for-shared-link) - [Create a Shared Link](#create-a-shared-link) - [Get a Shared Link](#get-a-shared-link) - [Update a Shared Link](#update-a-shared-link) @@ -681,6 +682,44 @@ BoxItem.Info itemInfo = BoxItem.getSharedItem(api, sharedLink, password); [get-shared-item]: http://opensource.box.com/box-java-sdk/javadoc/com/box/sdk/BoxItem.html#getSharedItem-com.box.sdk.BoxAPIConnection-java.lang.String- [get-shared-item-password]: http://opensource.box.com/box-java-sdk/javadoc/com/box/sdk/BoxItem.html#getSharedItem-com.box.sdk.BoxAPIConnection-java.lang.String-java.lang.String- +Download File from Shared Link +--------------- + +A file can be downloaded via a shared link +by calling [`downloadFromSharedLink(BoxAPIConnection api, OutputStream output, String sharedLink)`][download-from-shared-link] +and providing an `OutputStream` where the file's contents will be written and shared link of the file. + +If the shared link is password-protected, call +[`downloadFromSharedLink(BoxAPIConnection api, OutputStream output, String sharedLink, String password)`][download-from-shared-link-password] +method. + +```java +FileOutputStream stream = new FileOutputStream("My File.txt"); +String sharedLink = "https://cloud.box.com/s/12339wbq4c7y2xd3drg4j9j9wer3ptt6n"; +String password = "Secret123@"; +BoxFile.downloadFromSharedLink(api, stream, sharedLink, password); +stream.close(); +``` + +Download progress can be tracked by providing a [`ProgressListener`][progress] +to [` downloadFromSharedLink(BoxAPIConnection api, OutputStream output, String sharedLink, String password, ProgressListener listener)`][download-from-shared-link-password-progress]. +The `ProgressListener` will then receive progress updates as the download +completes. + +```java +FileOutputStream stream = new FileOutputStream("My File.txt"); +// Provide a ProgressListener to monitor the progress of the download. +BoxFile.downloadFromSharedLink(api, stream, sharedLink, password, new ProgressListener() { + public void onProgressChanged(long numBytes, long totalBytes) { + double percentComplete = numBytes / totalBytes; + } +}); +stream.close(); +``` +[download-from-shared-link]: http://opensource.box.com/box-java-sdk/javadoc/com/box/sdk/BoxFile.html#downloadFromSharedLink-com.box.sdk.BoxAPIConnection-java.io.OutputStream-java.lang.String- +[download-from-shared-link-password]: http://opensource.box.com/box-java-sdk/javadoc/com/box/sdk/BoxFile.html#downloadFromSharedLink-com.box.sdk.BoxAPIConnection-java.io.OutputStream-java.lang.String-java.lang.String- +[download-from-shared-link-password-progress]: http://opensource.box.com/box-java-sdk/javadoc/com/box/sdk/BoxFile.html#downloadFromSharedLink-com.box.sdk.BoxAPIConnection-java.io.OutputStream-java.lang.String-java.lang.String-com.box.sdk.ProgressListener- + Create a Shared Link -------------------- @@ -719,9 +758,9 @@ Retrieve the shared link for a file by calling ```java BoxFile file = new BoxFile(api, "id"); -BoxFile.Info info = file.getInfo() -BoxSharedLink link = info.getSharedLink() -String url = link.getUrl() +BoxFile.Info info = file.getInfo(); +BoxSharedLink link = info.getSharedLink(); +String url = link.getUrl(); ``` [get-shared-link]: http://opensource.box.com/box-java-sdk/javadoc/com/box/sdk/BoxItem.Info.html#getSharedLink-- diff --git a/src/intTest/java/com/box/sdk/BoxFileIT.java b/src/intTest/java/com/box/sdk/BoxFileIT.java index 6fa76e409..b1bbbd653 100644 --- a/src/intTest/java/com/box/sdk/BoxFileIT.java +++ b/src/intTest/java/com/box/sdk/BoxFileIT.java @@ -734,6 +734,36 @@ public void createAndUpdateSharedLinkSucceeds() { } } + @Test + public void downloadpdateSharedLinkSucceeds() throws IOException { + BoxAPIConnection api = jwtApiForServiceAccount(); + String fileName = "[downloadpdateSharedLinkSucceeds] Test File.txt"; + String fileContent = "Test file"; + String password = "Secret123@"; + BoxFile uploadedFile = null; + try { + uploadedFile = uploadFileToUniqueFolder(api, fileName, fileContent); + assertThat( + uploadedFile.getInfo("is_accessible_via_shared_link").getIsAccessibleViaSharedLink(), + is(false) + ); + BoxSharedLink sharedLink = uploadedFile.createSharedLink( + new BoxSharedLinkRequest() + .access(OPEN) + .password(password) + .permissions(true, true, true) + ); + + ByteArrayOutputStream downloadStream = new ByteArrayOutputStream(); + BoxFile.downloadFromSharedLink(api, downloadStream, sharedLink.getURL(), password); + downloadStream.close(); + byte[] downloadedFileContent = downloadStream.toByteArray(); + assertThat(downloadedFileContent, is(equalTo(fileContent.getBytes()))); + } finally { + deleteFile(uploadedFile); + } + } + @Test public void createEditableSharedLinkSucceeds() { BoxAPIConnection api = jwtApiForServiceAccount(); diff --git a/src/main/java/com/box/sdk/BoxFile.java b/src/main/java/com/box/sdk/BoxFile.java index ca3d9883d..065aae424 100644 --- a/src/main/java/com/box/sdk/BoxFile.java +++ b/src/main/java/com/box/sdk/BoxFile.java @@ -312,6 +312,65 @@ public void download(OutputStream output, ProgressListener listener) { writeStream(response, output, listener); } + /** + * Downloads the content of the file to a given OutputStream using the provided shared link. + * @param api the API connection to be used to get download URL of the file. + * @param output the stream to where the file will be written. + * @param sharedLink the shared link of the file. + */ + public static void downloadFromSharedLink(BoxAPIConnection api, OutputStream output, String sharedLink) { + downloadFromSharedLink(api, output, sharedLink, null, null); + } + + /** + * Downloads the content of the file to a given OutputStream using the provided shared link. + * @param api the API connection to be used to get download URL of the file. + * @param output the stream to where the file will be written. + * @param sharedLink the shared link of the file. + * @param password the password for the shared link. + */ + public static void downloadFromSharedLink( + BoxAPIConnection api, OutputStream output, String sharedLink, String password + ) { + downloadFromSharedLink(api, output, sharedLink, password, null); + } + + /** + * Downloads the content of the file to a given OutputStream using the provided shared link. + * @param api the API connection to be used to get download URL of the file. + * @param output the stream to where the file will be written. + * @param sharedLink the shared link of the file. + * @param listener a listener for monitoring the download's progress. + */ + public static void downloadFromSharedLink( + BoxAPIConnection api, OutputStream output, String sharedLink, ProgressListener listener + ) { + downloadFromSharedLink(api, output, sharedLink, null, listener); + } + + /** + * Downloads the content of the file to a given OutputStream using the provided shared link. + * @param api the API connection to be used to get download URL of the file. + * @param output the stream to where the file will be written. + * @param sharedLink the shared link of the file. + * @param password the password for the shared link. + * @param listener a listener for monitoring the download's progress. + */ + public static void downloadFromSharedLink( + BoxAPIConnection api, OutputStream output, String sharedLink, String password, ProgressListener listener + ) { + BoxItem.Info item = BoxItem.getSharedItem(api, sharedLink, password, "id"); + if (!(item instanceof BoxFile.Info)) { + throw new BoxAPIException("The shared link provided is not a shared link for a file."); + } + BoxFile sharedFile = new BoxFile(api, item.getID()); + URL url = sharedFile.getDownloadUrl(); + BoxAPIRequest request = new BoxAPIRequest(api, url, "GET"); + request.addHeader("BoxApi", BoxSharedLink.getSharedLinkHeaderValue(sharedLink, password)); + BoxAPIResponse response = request.send(); + writeStream(response, output, listener); + } + /** * Downloads a part of this file's contents, starting at specified byte offset. * diff --git a/src/main/java/com/box/sdk/BoxItem.java b/src/main/java/com/box/sdk/BoxItem.java index b867eab29..8b14a1321 100644 --- a/src/main/java/com/box/sdk/BoxItem.java +++ b/src/main/java/com/box/sdk/BoxItem.java @@ -60,11 +60,18 @@ public static BoxItem.Info getSharedItem(BoxAPIConnection api, String sharedLink * * @param api the API connection to be used by the shared item. * @param sharedLink the shared link to the item. - * @param password the password for the shared link. + * @param password the password for the shared link. Use `null` if shared link has no password. + * @param fields the fields to retrieve. * @return info about the shared item. */ - public static BoxItem.Info getSharedItem(BoxAPIConnection api, String sharedLink, String password) { - URL url = SHARED_ITEM_URL_TEMPLATE.build(api.getBaseURL()); + public static BoxItem.Info getSharedItem( + BoxAPIConnection api, String sharedLink, String password, String... fields + ) { + QueryStringBuilder builder = new QueryStringBuilder(); + if (fields.length > 0) { + builder.appendParam("fields", fields); + } + URL url = SHARED_ITEM_URL_TEMPLATE.buildWithQuery(api.getBaseURL(), builder.toString()); BoxJSONRequest request = new BoxJSONRequest(api, url, "GET"); request.addHeader("BoxApi", BoxSharedLink.getSharedLinkHeaderValue(sharedLink, password)); @@ -213,6 +220,7 @@ public abstract class Info extends BoxResource.Info { private String itemStatus; private Date expiresAt; private Set collections; + private String downloadUrl; /** * Constructs an empty Info object. @@ -492,6 +500,14 @@ public Iterable getCollections() { return this.collections; } + /*** + * Gets URL that can be used to download the file. + * @return + */ + public String getDownloadUrl() { + return this.downloadUrl; + } + /** * Sets the collections that this item belongs to. * @@ -613,6 +629,9 @@ protected void parseJSONMember(JsonObject.Member member) { this.collections.add(collectionInfo); } break; + case "download_url": + this.downloadUrl = value.asString(); + break; default: break; } diff --git a/src/main/java/com/box/sdk/SharedLinkAPIConnection.java b/src/main/java/com/box/sdk/SharedLinkAPIConnection.java index fbdd48f78..b3e2450d5 100644 --- a/src/main/java/com/box/sdk/SharedLinkAPIConnection.java +++ b/src/main/java/com/box/sdk/SharedLinkAPIConnection.java @@ -3,7 +3,7 @@ /** * This API connection uses a shared link (along with an optional password) to authenticate with the Box API. It wraps a * preexisting BoxAPIConnection in order to provide additional access to items that are accessible with a shared link. - * @deprecated Use {@link BoxItem#getSharedItem(BoxAPIConnection, String, String)} instead + * @deprecated Use {@link BoxItem#getSharedItem(BoxAPIConnection, String, String, String...)} instead */ public class SharedLinkAPIConnection extends BoxAPIConnection { private final BoxAPIConnection wrappedConnection; diff --git a/src/test/java/com/box/sdk/BoxFileTest.java b/src/test/java/com/box/sdk/BoxFileTest.java index 62f7479bb..09a42ea73 100644 --- a/src/test/java/com/box/sdk/BoxFileTest.java +++ b/src/test/java/com/box/sdk/BoxFileTest.java @@ -5,6 +5,9 @@ import static com.box.sdk.http.ContentType.APPLICATION_JSON; import static com.box.sdk.http.ContentType.APPLICATION_JSON_PATCH; import static com.box.sdk.http.ContentType.APPLICATION_OCTET_STREAM; +import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; import static java.lang.String.format; import static java.nio.charset.StandardCharsets.UTF_8; @@ -12,6 +15,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -728,6 +732,103 @@ public void createEditableSharedLinkSucceeds() { assertTrue(sharedLink.getPermissions().getCanEdit()); } + @Test + public void testDownloadFromSharedLinkWithPassword() { + final String sharedItemsURL = "/2.0/shared_items"; + final String fileContentURL = "/2.0/files/12345/content"; + final String sharedLink = "https://app.box.com/s/abcdef123456"; + final String password = "password"; + final byte[] fileContent = "This is a test file content".getBytes(); + final String expectedSharedLinkHeaderValue = "shared_link=" + sharedLink + "&shared_link_password=" + password; + final String expectedDownloadPath = "/shared/static/rh935iit6ewrmw0unyul.jpeg"; + final String expectedDownloadUrl = format("https://localhost:%d%s", wireMockRule.httpsPort(), expectedDownloadPath); + + String sharedItemsResponse = "{ \"type\": \"file\", \"id\": \"12345\" }"; + + wireMockRule.stubFor(WireMock.get(WireMock.urlPathEqualTo(sharedItemsURL)) + .willReturn(WireMock.aResponse() + .withHeader("Content-Type", APPLICATION_JSON) + .withBody(sharedItemsResponse))); + + wireMockRule.stubFor(WireMock.get(WireMock.urlPathEqualTo(fileContentURL)) + .withHeader("boxapi", WireMock.equalTo(expectedSharedLinkHeaderValue)) + .willReturn(WireMock.aResponse() + .withStatus(302) + .withHeader("Location", expectedDownloadUrl))); + + wireMockRule.stubFor(WireMock.get(WireMock.urlPathEqualTo(expectedDownloadPath)) + .willReturn(WireMock.aResponse() + .withHeader("Content-Type", "application/octet-stream") + .withBody(fileContent))); + + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + BoxFile.downloadFromSharedLink(api, output, sharedLink, password); + + verify(1, getRequestedFor( + urlEqualTo("/2.0/shared_items?fields=id")). + withHeader("BoxApi", WireMock.equalTo(expectedSharedLinkHeaderValue))); + + verify(1, getRequestedFor(urlEqualTo(fileContentURL)). + withHeader("boxapi", WireMock.equalTo(expectedSharedLinkHeaderValue))); + + verify(1, getRequestedFor(urlEqualTo(expectedDownloadPath))); + + assertArrayEquals(fileContent, output.toByteArray()); + } + + @Test + public void testDownloadFromSharedLinkWithProgressListener() { + final String sharedItemsURL = "/2.0/shared_items"; + final String fileContentURL = "/2.0/files/12345/content"; + final String sharedLink = "https://app.box.com/s/abcdef123456"; + final byte[] fileContent = "This is a test file content".getBytes(); + final String expectedSharedLinkHeaderValue = "shared_link=" + sharedLink; + final String expectedDownloadPath = "/shared/static/rh935iit6ewrmw0unyul.jpeg"; + final String expectedDownloadUrl = format( + "https://localhost:%d%s", wireMockRule.httpsPort(), expectedDownloadPath + ); + + String sharedItemsResponse = format( + "{ \"download_url\": \"%s\", \"type\": \"file\", \"id\": \"12345\" }", + expectedDownloadUrl + ); + + wireMockRule.stubFor(WireMock.get(WireMock.urlPathEqualTo(sharedItemsURL)) + .willReturn(WireMock.aResponse() + .withHeader("Content-Type", APPLICATION_JSON) + .withBody(sharedItemsResponse))); + + wireMockRule.stubFor(WireMock.get(WireMock.urlPathEqualTo(fileContentURL)) + .withHeader("boxapi", WireMock.equalTo(expectedSharedLinkHeaderValue)) + .willReturn(WireMock.aResponse() + .withStatus(302) + .withHeader("Location", expectedDownloadUrl))); + + wireMockRule.stubFor(WireMock.get(WireMock.urlPathEqualTo(expectedDownloadPath)) + .willReturn(WireMock.aResponse() + .withHeader("Content-Type", "application/octet-stream") + .withBody(fileContent))); + + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + ProgressListener listener = (numBytes, totalBytes) -> { + // Implement progress listener logic if needed + }; + BoxFile.downloadFromSharedLink(api, output, sharedLink, listener); + + verify(1, getRequestedFor( + urlEqualTo("/2.0/shared_items?fields=id")). + withHeader("BoxApi", WireMock.equalTo(expectedSharedLinkHeaderValue))); + + verify(1, getRequestedFor(urlEqualTo(fileContentURL)). + withHeader("boxapi", WireMock.equalTo(expectedSharedLinkHeaderValue))); + + verify(1, getRequestedFor(urlEqualTo(expectedDownloadPath))); + + assertArrayEquals(fileContent, output.toByteArray()); + } + @Test public void testAddClassification() { final String fileID = "12345";