Skip to content

Commit 9b7f28b

Browse files
feat: Support downloading file from shared link (#1282)
--------- Co-authored-by: Minh Nguyen Cong <[email protected]>
1 parent d9564e2 commit 9b7f28b

File tree

6 files changed

+255
-7
lines changed

6 files changed

+255
-7
lines changed

doc/files.md

Lines changed: 42 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,44 @@ 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 from 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]: http://opensource.box.com/box-java-sdk/javadoc/com/box/sdk/BoxFile.html#downloadFromSharedLink-com.box.sdk.BoxAPIConnection-java.io.OutputStream-java.lang.String-
720+
[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-
721+
[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-
722+
684723
Create a Shared Link
685724
--------------------
686725

@@ -719,9 +758,9 @@ Retrieve the shared link for a file by calling
719758
<!-- sample get_files_id get_shared_link -->
720759
```java
721760
BoxFile file = new BoxFile(api, "id");
722-
BoxFile.Info info = file.getInfo()
723-
BoxSharedLink link = info.getSharedLink()
724-
String url = link.getUrl()
761+
BoxFile.Info info = file.getInfo();
762+
BoxSharedLink link = info.getSharedLink();
763+
String url = link.getUrl();
725764
```
726765

727766
[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 & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,6 +734,36 @@ public void createAndUpdateSharedLinkSucceeds() {
734734
}
735735
}
736736

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

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

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,65 @@ 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, "id");
363+
if (!(item instanceof BoxFile.Info)) {
364+
throw new BoxAPIException("The shared link provided is not a shared link for a file.");
365+
}
366+
BoxFile sharedFile = new BoxFile(api, item.getID());
367+
URL url = sharedFile.getDownloadUrl();
368+
BoxAPIRequest request = new BoxAPIRequest(api, url, "GET");
369+
request.addHeader("BoxApi", BoxSharedLink.getSharedLinkHeaderValue(sharedLink, password));
370+
BoxAPIResponse response = request.send();
371+
writeStream(response, output, listener);
372+
}
373+
315374
/**
316375
* Downloads a part of this file's contents, starting at specified byte offset.
317376
*

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

0 commit comments

Comments
 (0)