Skip to content

Commit 991c0f3

Browse files
committed
feat: Support downloading file by shared link
1 parent d9564e2 commit 991c0f3

File tree

6 files changed

+239
-10
lines changed

6 files changed

+239
-10
lines changed

doc/files.md

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ file's contents, upload new versions, and perform other common file operations
2929
- [Lock a File](#lock-a-file)
3030
- [Unlock a File](#unlock-a-file)
3131
- [Find File for Shared Link](#find-file-for-shared-link)
32+
- [Download File for Shared Link](#download-file-for-shared-link)
3233
- [Create a Shared Link](#create-a-shared-link)
3334
- [Get a Shared Link](#get-a-shared-link)
3435
- [Update a Shared Link](#update-a-shared-link)
@@ -681,6 +682,43 @@ BoxItem.Info itemInfo = BoxItem.getSharedItem(api, sharedLink, password);
681682
[get-shared-item]: http://opensource.box.com/box-java-sdk/javadoc/com/box/sdk/BoxItem.html#getSharedItem-com.box.sdk.BoxAPIConnection-java.lang.String-
682683
[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-
683684

685+
Download File for Shared Link
686+
---------------
687+
688+
A file can be downloaded via a shared link
689+
by calling [`downloadFromSharedLink(BoxAPIConnection api, OutputStream output, String sharedLink)`][download-from-shared-link]
690+
and providing an `OutputStream` where the file's contents will be written and shared link of the file.
691+
692+
If the shared link is password-protected, call
693+
[`downloadFromSharedLink(BoxAPIConnection api, OutputStream output, String sharedLink, String password)`][download-from-shared-link-password]
694+
method.
695+
696+
```java
697+
FileOutputStream stream = new FileOutputStream("My File.txt");
698+
String sharedLink = "https://cloud.box.com/s/12339wbq4c7y2xd3drg4j9j9wer3ptt6n";
699+
String password = "Secret123@";
700+
BoxFile.downloadFromSharedLink(api, stream, sharedLink, password);
701+
stream.close();
702+
```
703+
704+
Download progress can be tracked by providing a [`ProgressListener`][progress]
705+
to [` downloadFromSharedLink(BoxAPIConnection api, OutputStream output, String sharedLink, String password, ProgressListener listener)`][download-from-shared-link-password-progress].
706+
The `ProgressListener` will then receive progress updates as the download
707+
completes.
708+
709+
```java
710+
FileOutputStream stream = new FileOutputStream("My File.txt");
711+
// Provide a ProgressListener to monitor the progress of the download.
712+
BoxFile.downloadFromSharedLink(api, stream, sharedLink, password, new ProgressListener() {
713+
public void onProgressChanged(long numBytes, long totalBytes) {
714+
double percentComplete = numBytes / totalBytes;
715+
}
716+
});
717+
stream.close();
718+
```
719+
[download-from-shared-link-password]: http://opensource.box.com/box-java-sdk/javadoc/com/box/sdk/BoxFile.html#createSharedLink-com.box.sdk.sharedlink.BoxSharedLinkRequest-
720+
[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-
721+
684722
Create a Shared Link
685723
--------------------
686724

@@ -719,9 +757,9 @@ Retrieve the shared link for a file by calling
719757
<!-- sample get_files_id get_shared_link -->
720758
```java
721759
BoxFile file = new BoxFile(api, "id");
722-
BoxFile.Info info = file.getInfo()
723-
BoxSharedLink link = info.getSharedLink()
724-
String url = link.getUrl()
760+
BoxFile.Info info = file.getInfo();
761+
BoxSharedLink link = info.getSharedLink();
762+
String url = link.getUrl();
725763
```
726764

727765
[get-shared-link]: http://opensource.box.com/box-java-sdk/javadoc/com/box/sdk/BoxItem.Info.html#getSharedLink--

src/intTest/java/com/box/sdk/BoxFileIT.java

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -216,9 +216,6 @@ public void uploadAndDownloadFileSucceeds() throws IOException {
216216
byte[] downloadedFileContent = downloadStream.toByteArray();
217217

218218
assertThat(downloadedFileContent, is(equalTo(fileContent)));
219-
assertThat(folder, hasItem(Matchers.<BoxItem.Info>hasProperty("ID", equalTo(uploadedFile.getID()))));
220-
verify(mockUploadListener, atLeastOnce()).onProgressChanged(anyLong(), longThat(is(equalTo(fileSize))));
221-
verify(mockDownloadListener, atLeastOnce()).onProgressChanged(anyLong(), longThat(is(equalTo(fileSize))));
222219
} finally {
223220
deleteFile(uploadedFile);
224221
}
@@ -734,6 +731,36 @@ public void createAndUpdateSharedLinkSucceeds() {
734731
}
735732
}
736733

734+
@Test
735+
public void downloadpdateSharedLinkSucceeds() throws IOException {
736+
BoxAPIConnection api = jwtApiForServiceAccount();
737+
String fileName = "[downloadpdateSharedLinkSucceeds] Test File.txt";
738+
String fileContent = "Test file";
739+
String password = "Secret123@";
740+
BoxFile uploadedFile = null;
741+
try {
742+
uploadedFile = uploadFileToUniqueFolder(api, fileName, fileContent);
743+
assertThat(
744+
uploadedFile.getInfo("is_accessible_via_shared_link").getIsAccessibleViaSharedLink(),
745+
is(false)
746+
);
747+
BoxSharedLink sharedLink = uploadedFile.createSharedLink(
748+
new BoxSharedLinkRequest()
749+
.access(OPEN)
750+
.password(password)
751+
.permissions(true, true, true)
752+
);
753+
754+
ByteArrayOutputStream downloadStream = new ByteArrayOutputStream();
755+
BoxFile.downloadFromSharedLink(api, downloadStream, sharedLink.getURL(), password);
756+
downloadStream.close();
757+
byte[] downloadedFileContent = downloadStream.toByteArray();
758+
assertThat(downloadedFileContent, is(equalTo(fileContent.getBytes())));
759+
} finally {
760+
deleteFile(uploadedFile);
761+
}
762+
}
763+
737764
@Test
738765
public void createEditableSharedLinkSucceeds() {
739766
BoxAPIConnection api = jwtApiForServiceAccount();

src/main/java/com/box/sdk/BoxFile.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,67 @@ public void download(OutputStream output, ProgressListener listener) {
312312
writeStream(response, output, listener);
313313
}
314314

315+
/**
316+
* Downloads the content of the file to a given OutputStream using the provided shared link.
317+
* @param api the API connection to be used to get download URL of the file.
318+
* @param output the stream to where the file will be written.
319+
* @param sharedLink the shared link of the file.
320+
*/
321+
public static void downloadFromSharedLink(BoxAPIConnection api, OutputStream output, String sharedLink) {
322+
downloadFromSharedLink(api, output, sharedLink, null, null);
323+
}
324+
325+
/**
326+
* Downloads the content of the file to a given OutputStream using the provided shared link.
327+
* @param api the API connection to be used to get download URL of the file.
328+
* @param output the stream to where the file will be written.
329+
* @param sharedLink the shared link of the file.
330+
* @param password the password for the shared link.
331+
*/
332+
public static void downloadFromSharedLink(
333+
BoxAPIConnection api, OutputStream output, String sharedLink, String password
334+
) {
335+
downloadFromSharedLink(api, output, sharedLink, password, null);
336+
}
337+
338+
/**
339+
* Downloads the content of the file to a given OutputStream using the provided shared link.
340+
* @param api the API connection to be used to get download URL of the file.
341+
* @param output the stream to where the file will be written.
342+
* @param sharedLink the shared link of the file.
343+
* @param listener a listener for monitoring the download's progress.
344+
*/
345+
public static void downloadFromSharedLink(
346+
BoxAPIConnection api, OutputStream output, String sharedLink, ProgressListener listener
347+
) {
348+
downloadFromSharedLink(api, output, sharedLink, null, listener);
349+
}
350+
351+
/**
352+
* Downloads the content of the file to a given OutputStream using the provided shared link.
353+
* @param api the API connection to be used to get download URL of the file.
354+
* @param output the stream to where the file will be written.
355+
* @param sharedLink the shared link of the file.
356+
* @param password the password for the shared link.
357+
* @param listener a listener for monitoring the download's progress.
358+
*/
359+
public static void downloadFromSharedLink(
360+
BoxAPIConnection api, OutputStream output, String sharedLink, String password, ProgressListener listener
361+
) {
362+
BoxItem.Info item = BoxItem.getSharedItem(api, sharedLink, password, "download_url");
363+
URL url;
364+
try {
365+
url = new URL(item.getDownloadUrl());
366+
} catch (MalformedURLException e) {
367+
throw new RuntimeException(
368+
String.format("Invalid download URL %s for shared link %s", item.getDownloadUrl(), sharedLink), e
369+
);
370+
}
371+
BoxAPIRequest request = new BoxAPIRequest(api, url, "GET");
372+
BoxAPIResponse response = request.send();
373+
writeStream(response, output, listener);
374+
}
375+
315376
/**
316377
* Downloads a part of this file's contents, starting at specified byte offset.
317378
*

src/main/java/com/box/sdk/BoxItem.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,18 @@ public static BoxItem.Info getSharedItem(BoxAPIConnection api, String sharedLink
6060
*
6161
* @param api the API connection to be used by the shared item.
6262
* @param sharedLink the shared link to the item.
63-
* @param password the password for the shared link.
63+
* @param password the password for the shared link. Use `null` if shared link has no password.
64+
* @param fields the fields to retrieve.
6465
* @return info about the shared item.
6566
*/
66-
public static BoxItem.Info getSharedItem(BoxAPIConnection api, String sharedLink, String password) {
67-
URL url = SHARED_ITEM_URL_TEMPLATE.build(api.getBaseURL());
67+
public static BoxItem.Info getSharedItem(
68+
BoxAPIConnection api, String sharedLink, String password, String... fields
69+
) {
70+
QueryStringBuilder builder = new QueryStringBuilder();
71+
if (fields.length > 0) {
72+
builder.appendParam("fields", fields);
73+
}
74+
URL url = SHARED_ITEM_URL_TEMPLATE.buildWithQuery(api.getBaseURL(), builder.toString());
6875
BoxJSONRequest request = new BoxJSONRequest(api, url, "GET");
6976

7077
request.addHeader("BoxApi", BoxSharedLink.getSharedLinkHeaderValue(sharedLink, password));
@@ -213,6 +220,7 @@ public abstract class Info extends BoxResource.Info {
213220
private String itemStatus;
214221
private Date expiresAt;
215222
private Set<BoxCollection.Info> collections;
223+
private String downloadUrl;
216224

217225
/**
218226
* Constructs an empty Info object.
@@ -492,6 +500,14 @@ public Iterable<BoxCollection.Info> getCollections() {
492500
return this.collections;
493501
}
494502

503+
/***
504+
* Gets URL that can be used to download the file.
505+
* @return
506+
*/
507+
public String getDownloadUrl() {
508+
return this.downloadUrl;
509+
}
510+
495511
/**
496512
* Sets the collections that this item belongs to.
497513
*
@@ -613,6 +629,9 @@ protected void parseJSONMember(JsonObject.Member member) {
613629
this.collections.add(collectionInfo);
614630
}
615631
break;
632+
case "download_url":
633+
this.downloadUrl = value.asString();
634+
break;
616635
default:
617636
break;
618637
}

src/main/java/com/box/sdk/SharedLinkAPIConnection.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
/**
44
* This API connection uses a shared link (along with an optional password) to authenticate with the Box API. It wraps a
55
* preexisting BoxAPIConnection in order to provide additional access to items that are accessible with a shared link.
6-
* @deprecated Use {@link BoxItem#getSharedItem(BoxAPIConnection, String, String)} instead
6+
* @deprecated Use {@link BoxItem#getSharedItem(BoxAPIConnection, String, String, String...)} instead
77
*/
88
public class SharedLinkAPIConnection extends BoxAPIConnection {
99
private final BoxAPIConnection wrappedConnection;

src/test/java/com/box/sdk/BoxFileTest.java

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@
55
import static com.box.sdk.http.ContentType.APPLICATION_JSON;
66
import static com.box.sdk.http.ContentType.APPLICATION_JSON_PATCH;
77
import static com.box.sdk.http.ContentType.APPLICATION_OCTET_STREAM;
8+
import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
9+
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
10+
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
811
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
912
import static java.lang.String.format;
1013
import static java.nio.charset.StandardCharsets.UTF_8;
1114
import static org.hamcrest.MatcherAssert.assertThat;
1215
import static org.hamcrest.Matchers.containsString;
1316
import static org.hamcrest.Matchers.equalTo;
1417
import static org.hamcrest.Matchers.is;
18+
import static org.junit.Assert.assertArrayEquals;
1519
import static org.junit.Assert.assertEquals;
1620
import static org.junit.Assert.assertNull;
1721
import static org.junit.Assert.assertTrue;
@@ -728,6 +732,86 @@ public void createEditableSharedLinkSucceeds() {
728732
assertTrue(sharedLink.getPermissions().getCanEdit());
729733
}
730734

735+
@Test
736+
public void testDownloadFromSharedLinkWithPassword() {
737+
final String sharedItemsURL = "/2.0/shared_items";
738+
final String sharedLink = "https://app.box.com/s/abcdef123456";
739+
final String password = "password";
740+
final byte[] fileContent = "This is a test file content".getBytes();
741+
final String expectedSharedLinkHeaderValue = "shared_link=" + sharedLink + "&shared_link_password=" + password;
742+
final String expectedDownloadPath = "/shared/static/rh935iit6ewrmw0unyul.jpeg";
743+
final String expectedDownloadUrl = format("https://localhost:%d%s", wireMockRule.httpsPort(), expectedDownloadPath);
744+
745+
String sharedItemsResponse = format(
746+
"{ \"download_url\": \"%s\", \"type\": \"file\", \"id\": \"12345\" }",
747+
expectedDownloadUrl
748+
);
749+
750+
wireMockRule.stubFor(WireMock.get(WireMock.urlPathEqualTo(sharedItemsURL))
751+
.willReturn(WireMock.aResponse()
752+
.withHeader("Content-Type", APPLICATION_JSON)
753+
.withBody(sharedItemsResponse)));
754+
755+
wireMockRule.stubFor(WireMock.get(WireMock.urlPathEqualTo(expectedDownloadPath))
756+
.willReturn(WireMock.aResponse()
757+
.withHeader("Content-Type", "application/octet-stream")
758+
.withBody(fileContent)));
759+
760+
761+
ByteArrayOutputStream output = new ByteArrayOutputStream();
762+
BoxFile.downloadFromSharedLink(api, output, sharedLink, password);
763+
764+
verify(1, getRequestedFor(
765+
urlEqualTo("/2.0/shared_items?fields=download_url")).
766+
withHeader("BoxApi", WireMock.equalTo(expectedSharedLinkHeaderValue)));
767+
768+
verify(1, getRequestedFor(urlEqualTo(expectedDownloadPath)));
769+
770+
assertArrayEquals(fileContent, output.toByteArray());
771+
}
772+
773+
@Test
774+
public void testDownloadFromSharedLinkWithProgressListener() {
775+
final String sharedItemsURL = "/2.0/shared_items";
776+
final String sharedLink = "https://app.box.com/s/abcdef123456";
777+
final byte[] fileContent = "This is a test file content".getBytes();
778+
final String expectedSharedLinkHeaderValue = "shared_link=" + sharedLink;
779+
final String expectedDownloadPath = "/shared/static/rh935iit6ewrmw0unyul.jpeg";
780+
final String expectedDownloadUrl = format(
781+
"https://localhost:%d%s", wireMockRule.httpsPort(), expectedDownloadPath
782+
);
783+
784+
String sharedItemsResponse = format(
785+
"{ \"download_url\": \"%s\", \"type\": \"file\", \"id\": \"12345\" }",
786+
expectedDownloadUrl
787+
);
788+
789+
wireMockRule.stubFor(WireMock.get(WireMock.urlPathEqualTo(sharedItemsURL))
790+
.willReturn(WireMock.aResponse()
791+
.withHeader("Content-Type", APPLICATION_JSON)
792+
.withBody(sharedItemsResponse)));
793+
794+
wireMockRule.stubFor(WireMock.get(WireMock.urlPathEqualTo(expectedDownloadPath))
795+
.willReturn(WireMock.aResponse()
796+
.withHeader("Content-Type", "application/octet-stream")
797+
.withBody(fileContent)));
798+
799+
800+
ByteArrayOutputStream output = new ByteArrayOutputStream();
801+
ProgressListener listener = (numBytes, totalBytes) -> {
802+
// Implement progress listener logic if needed
803+
};
804+
BoxFile.downloadFromSharedLink(api, output, sharedLink, listener);
805+
806+
verify(1, getRequestedFor(
807+
urlEqualTo("/2.0/shared_items?fields=download_url")).
808+
withHeader("BoxApi", WireMock.equalTo(expectedSharedLinkHeaderValue)));
809+
810+
verify(1, getRequestedFor(urlEqualTo(expectedDownloadPath)));
811+
812+
assertArrayEquals(fileContent, output.toByteArray());
813+
}
814+
731815
@Test
732816
public void testAddClassification() {
733817
final String fileID = "12345";

0 commit comments

Comments
 (0)