Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support downloading file from shared link #1282

Merged
merged 4 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 42 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,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
--------------------

Expand Down Expand Up @@ -719,9 +758,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
30 changes: 30 additions & 0 deletions src/intTest/java/com/box/sdk/BoxFileIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
59 changes: 59 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,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.
*
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
101 changes: 101 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,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";
Expand Down
Loading