Skip to content

Commit

Permalink
feat: Support downloading file by shared link
Browse files Browse the repository at this point in the history
  • Loading branch information
lukaszsocha2 committed Dec 9, 2024
1 parent d9564e2 commit 991c0f3
Show file tree
Hide file tree
Showing 6 changed files with 239 additions and 10 deletions.
44 changes: 41 additions & 3 deletions doc/files.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -681,6 +682,43 @@ 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 for 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-password]: http://opensource.box.com/box-java-sdk/javadoc/com/box/sdk/BoxFile.html#createSharedLink-com.box.sdk.sharedlink.BoxSharedLinkRequest-
[download-from-shared-link-password-progress]: http://opensource.box.com/box-java-sdk/javadoc/com/box/sdk/BoxFile.html#createSharedLink-com.box.sdk.sharedlink.BoxSharedLinkRequest-

Create a Shared Link
--------------------

Expand Down Expand Up @@ -719,9 +757,9 @@ Retrieve the shared link for a file by calling
<!-- sample get_files_id get_shared_link -->
```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--
Expand Down
33 changes: 30 additions & 3 deletions src/intTest/java/com/box/sdk/BoxFileIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -216,9 +216,6 @@ public void uploadAndDownloadFileSucceeds() throws IOException {
byte[] downloadedFileContent = downloadStream.toByteArray();

assertThat(downloadedFileContent, is(equalTo(fileContent)));
assertThat(folder, hasItem(Matchers.<BoxItem.Info>hasProperty("ID", equalTo(uploadedFile.getID()))));
verify(mockUploadListener, atLeastOnce()).onProgressChanged(anyLong(), longThat(is(equalTo(fileSize))));
verify(mockDownloadListener, atLeastOnce()).onProgressChanged(anyLong(), longThat(is(equalTo(fileSize))));
} finally {
deleteFile(uploadedFile);
}
Expand Down Expand Up @@ -734,6 +731,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();
Expand Down
61 changes: 61 additions & 0 deletions src/main/java/com/box/sdk/BoxFile.java
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,67 @@ 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, "download_url");
URL url;
try {
url = new URL(item.getDownloadUrl());
} catch (MalformedURLException e) {
throw new RuntimeException(
String.format("Invalid download URL %s for shared link %s", item.getDownloadUrl(), sharedLink), e
);
}
BoxAPIRequest request = new BoxAPIRequest(api, url, "GET");
BoxAPIResponse response = request.send();
writeStream(response, output, listener);
}

/**
* Downloads a part of this file's contents, starting at specified byte offset.
*
Expand Down
25 changes: 22 additions & 3 deletions src/main/java/com/box/sdk/BoxItem.java
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -213,6 +220,7 @@ public abstract class Info extends BoxResource.Info {
private String itemStatus;
private Date expiresAt;
private Set<BoxCollection.Info> collections;
private String downloadUrl;

/**
* Constructs an empty Info object.
Expand Down Expand Up @@ -492,6 +500,14 @@ public Iterable<BoxCollection.Info> 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.
*
Expand Down Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/box/sdk/SharedLinkAPIConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
84 changes: 84 additions & 0 deletions src/test/java/com/box/sdk/BoxFileTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@
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;
import static org.hamcrest.MatcherAssert.assertThat;
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;
Expand Down Expand Up @@ -728,6 +732,86 @@ public void createEditableSharedLinkSucceeds() {
assertTrue(sharedLink.getPermissions().getCanEdit());
}

@Test
public void testDownloadFromSharedLinkWithPassword() {
final String sharedItemsURL = "/2.0/shared_items";
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 = 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(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=download_url")).
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 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(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=download_url")).
withHeader("BoxApi", WireMock.equalTo(expectedSharedLinkHeaderValue)));

verify(1, getRequestedFor(urlEqualTo(expectedDownloadPath)));

assertArrayEquals(fileContent, output.toByteArray());
}

@Test
public void testAddClassification() {
final String fileID = "12345";
Expand Down

0 comments on commit 991c0f3

Please sign in to comment.