Skip to content

Commit

Permalink
feat(filesystem): download files from server to filesystem
Browse files Browse the repository at this point in the history
* feat(filesystem): download files from server to filesystem

* swiftlint

* chore: swiftlint fixs

* chore: fix missing parentheses

* chore: swiftlint not respecting per project configs

* chore: add missing parentheses

* chore: pin capacitor versions

---------

Co-authored-by: IT-MikeS <20338451+IT-MikeS@users.noreply.github.com>
  • Loading branch information
ItsChaceD and IT-MikeS committed Jul 12, 2023
1 parent 7e1911d commit d16bad6
Show file tree
Hide file tree
Showing 10 changed files with 559 additions and 9 deletions.
81 changes: 81 additions & 0 deletions filesystem/README.md
Expand Up @@ -101,6 +101,8 @@ const readFilePath = async () => {
* [`copy(...)`](#copy)
* [`checkPermissions()`](#checkpermissions)
* [`requestPermissions()`](#requestpermissions)
* [`downloadFile(...)`](#downloadfile)
* [`addListener('progress', ...)`](#addlistenerprogress)
* [Interfaces](#interfaces)
* [Type Aliases](#type-aliases)
* [Enums](#enums)
Expand Down Expand Up @@ -343,6 +345,45 @@ Required on Android, only when using <a href="#directory">`Directory.Documents`<
--------------------


### downloadFile(...)

```typescript
downloadFile(options: DownloadFileOptions) => Promise<DownloadFileResult>
```

Perform a http request to a server and download the file to the specified destination.

| Param | Type |
| ------------- | ------------------------------------------------------------------- |
| **`options`** | <code><a href="#downloadfileoptions">DownloadFileOptions</a></code> |

**Returns:** <code>Promise&lt;<a href="#downloadfileresult">DownloadFileResult</a>&gt;</code>

**Since:** 5.1.0

--------------------


### addListener('progress', ...)

```typescript
addListener(eventName: 'progress', listenerFunc: ProgressListener) => Promise<PluginListenerHandle> & PluginListenerHandle
```

Add a listener to file download progress events.

| Param | Type |
| ------------------ | ------------------------------------------------------------- |
| **`eventName`** | <code>'progress'</code> |
| **`listenerFunc`** | <code><a href="#progresslistener">ProgressListener</a></code> |

**Returns:** <code>Promise&lt;<a href="#pluginlistenerhandle">PluginListenerHandle</a>&gt; & <a href="#pluginlistenerhandle">PluginListenerHandle</a></code>

**Since:** 5.1.0

--------------------


### Interfaces


Expand Down Expand Up @@ -501,6 +542,39 @@ Required on Android, only when using <a href="#directory">`Directory.Documents`<
| **`publicStorage`** | <code><a href="#permissionstate">PermissionState</a></code> |


#### DownloadFileResult

| Prop | Type | Description | Since |
| ---------- | ------------------- | -------------------------------------------------------------------- | ----- |
| **`path`** | <code>string</code> | The path the file was downloaded to. | 5.1.0 |
| **`blob`** | <code>Blob</code> | The blob data of the downloaded file. This is only available on web. | 5.1.0 |


#### DownloadFileOptions

| Prop | Type | Description | Since |
| --------------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
| **`path`** | <code>string</code> | The path the downloaded file should be moved to. | 5.1.0 |
| **`directory`** | <code><a href="#directory">Directory</a></code> | The directory to write the file to. If this option is used, filePath can be a relative path rather than absolute. The default is the `DATA` directory. | 5.1.0 |
| **`progress`** | <code>boolean</code> | An optional listener function to receive downloaded progress events. If this option is used, progress event should be dispatched on every chunk received. Chunks are throttled to every 100ms on Android/iOS to avoid slowdowns. | 5.1.0 |


#### PluginListenerHandle

| Prop | Type |
| ------------ | ----------------------------------------- |
| **`remove`** | <code>() =&gt; Promise&lt;void&gt;</code> |


#### ProgressStatus

| Prop | Type | Description | Since |
| ------------------- | ------------------- | ---------------------------------------------------- | ----- |
| **`url`** | <code>string</code> | The url of the file being downloaded. | 5.1.0 |
| **`bytes`** | <code>number</code> | The number of bytes downloaded so far. | 5.1.0 |
| **`contentLength`** | <code>number</code> | The total number of bytes to download for this file. | 5.1.0 |


### Type Aliases


Expand All @@ -514,6 +588,13 @@ Required on Android, only when using <a href="#directory">`Directory.Documents`<
<code>'prompt' | 'prompt-with-rationale' | 'granted' | 'denied'</code>


#### ProgressListener

A listener function that receives progress events.

<code>(progress: <a href="#progressstatus">ProgressStatus</a>): void</code>


### Enums


Expand Down
Expand Up @@ -7,10 +7,27 @@
import com.capacitorjs.plugins.filesystem.exceptions.CopyFailedException;
import com.capacitorjs.plugins.filesystem.exceptions.DirectoryExistsException;
import com.capacitorjs.plugins.filesystem.exceptions.DirectoryNotFoundException;
import java.io.*;
import com.getcapacitor.Bridge;
import com.getcapacitor.JSObject;
import com.getcapacitor.PluginCall;
import com.getcapacitor.plugin.util.CapacitorHttpUrlConnection;
import com.getcapacitor.plugin.util.HttpRequestHandler;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import org.json.JSONException;

public class Filesystem {

Expand Down Expand Up @@ -285,4 +302,82 @@ public void copyRecursively(File src, File dst) throws IOException {
destination.transferFrom(source, 0, source.size());
}
}

public JSObject downloadFile(PluginCall call, Bridge bridge, HttpRequestHandler.ProgressEmitter emitter)
throws IOException, URISyntaxException, JSONException {
String urlString = call.getString("url", "");
JSObject headers = call.getObject("headers", new JSObject());
JSObject params = call.getObject("params", new JSObject());
Integer connectTimeout = call.getInt("connectTimeout");
Integer readTimeout = call.getInt("readTimeout");
Boolean disableRedirects = call.getBoolean("disableRedirects");
Boolean shouldEncode = call.getBoolean("shouldEncodeUrlParams", true);
Boolean progress = call.getBoolean("progress", false);

String method = call.getString("method", "GET").toUpperCase(Locale.ROOT);
String path = call.getString("path");
String directory = call.getString("directory", Environment.DIRECTORY_DOWNLOADS);

final URL url = new URL(urlString);
final File file = getFileObject(path, directory);

HttpRequestHandler.HttpURLConnectionBuilder connectionBuilder = new HttpRequestHandler.HttpURLConnectionBuilder()
.setUrl(url)
.setMethod(method)
.setHeaders(headers)
.setUrlParams(params, shouldEncode)
.setConnectTimeout(connectTimeout)
.setReadTimeout(readTimeout)
.setDisableRedirects(disableRedirects)
.openConnection();

CapacitorHttpUrlConnection connection = connectionBuilder.build();

connection.setSSLSocketFactory(bridge);

InputStream connectionInputStream = connection.getInputStream();
FileOutputStream fileOutputStream = new FileOutputStream(file, false);

String contentLength = connection.getHeaderField("content-length");
int bytes = 0;
int maxBytes = 0;

try {
maxBytes = contentLength != null ? Integer.parseInt(contentLength) : 0;
} catch (NumberFormatException ignored) {}

byte[] buffer = new byte[1024];
int len;

// Throttle emitter to 100ms so it doesn't slow down app
long lastEmitTime = System.currentTimeMillis();
long minEmitIntervalMillis = 100;

while ((len = connectionInputStream.read(buffer)) > 0) {
fileOutputStream.write(buffer, 0, len);

bytes += len;

if (progress && null != emitter) {
long currentTime = System.currentTimeMillis();
if (currentTime - lastEmitTime > minEmitIntervalMillis) {
emitter.emit(bytes, maxBytes);
lastEmitTime = currentTime;
}
}
}

if (progress && null != emitter) {
emitter.emit(bytes, maxBytes);
}

connectionInputStream.close();
fileOutputStream.close();

return new JSObject() {
{
put("path", file.getAbsolutePath());
}
};
}
}
Expand Up @@ -4,6 +4,7 @@
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import com.capacitorjs.plugins.filesystem.exceptions.CopyFailedException;
import com.capacitorjs.plugins.filesystem.exceptions.DirectoryExistsException;
import com.capacitorjs.plugins.filesystem.exceptions.DirectoryNotFoundException;
Expand All @@ -17,7 +18,10 @@
import com.getcapacitor.annotation.CapacitorPlugin;
import com.getcapacitor.annotation.Permission;
import com.getcapacitor.annotation.PermissionCallback;
import java.io.*;
import com.getcapacitor.plugin.util.HttpRequestHandler;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.attribute.BasicFileAttributes;
Expand Down Expand Up @@ -376,6 +380,31 @@ public void copy(PluginCall call) {
this._copy(call, false);
}

@PluginMethod
public void downloadFile(PluginCall call) {
try {
String directory = call.getString("directory", Environment.DIRECTORY_DOWNLOADS);

if (isPublicDirectory(directory) && !isStoragePermissionGranted()) {
requestAllPermissions(call, "permissionCallback");
} else {
HttpRequestHandler.ProgressEmitter emitter = (bytes, contentLength) -> {
JSObject ret = new JSObject();
ret.put("url", call.getString("url"));
ret.put("bytes", bytes);
ret.put("contentLength", contentLength);

notifyListeners("progress", ret);
};

JSObject response = implementation.downloadFile(call, bridge, emitter);
call.resolve(response);
}
} catch (Exception ex) {
call.reject("Error downloading file: " + ex.getLocalizedMessage(), ex);
}
}

private void _copy(PluginCall call, Boolean doRename) {
String from = call.getString("from");
String to = call.getString("to");
Expand Down Expand Up @@ -448,6 +477,9 @@ private void permissionCallback(PluginCall call) {
case "stat":
stat(call);
break;
case "downloadFile":
downloadFile(call);
break;
}
}

Expand Down

0 comments on commit d16bad6

Please sign in to comment.