@capgo/capacitor-downloader 8.1.31 → 8.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -13
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/ee/forgr/capacitor/plugin/downloader/CapacitorDownloaderPlugin.java +13 -3
- package/android/src/main/java/ee/forgr/capacitor/plugin/downloader/DownloadNotificationVisibility.java +20 -0
- package/dist/docs.json +15 -3
- package/dist/esm/definitions.d.ts +18 -1
- package/dist/esm/definitions.js.map +1 -1
- package/ios/Sources/CapacitorDownloaderPlugin/CapacitorDownloaderPlugin.swift +73 -12
- package/ios/Sources/CapacitorDownloaderPlugin/DownloadDestinationResolver.swift +24 -0
- package/ios/Sources/CapacitorDownloaderPlugin/DownloadDestinationStore.swift +36 -0
- package/ios/Sources/CapacitorDownloaderPlugin/DownloadHTTPValidator.swift +7 -0
- package/ios/Tests/CapacitorDownloaderPluginTests/CapacitorDownloaderPluginTests.swift +95 -8
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -187,16 +187,16 @@ Get information about a downloaded file.
|
|
|
187
187
|
### addListener('downloadProgress', ...)
|
|
188
188
|
|
|
189
189
|
```typescript
|
|
190
|
-
addListener(eventName: 'downloadProgress', listenerFunc: (progress: { id: string; progress: number; }) => void) => Promise<PluginListenerHandle>
|
|
190
|
+
addListener(eventName: 'downloadProgress', listenerFunc: (progress: { id: string; progress: number; bytesWritten?: number | undefined; bytesTotal?: number | undefined; }) => void) => Promise<PluginListenerHandle>
|
|
191
191
|
```
|
|
192
192
|
|
|
193
193
|
Listen for download progress updates.
|
|
194
194
|
Fired periodically as download progresses.
|
|
195
195
|
|
|
196
|
-
| Param | Type
|
|
197
|
-
| ------------------ |
|
|
198
|
-
| **`eventName`** | <code>'downloadProgress'</code>
|
|
199
|
-
| **`listenerFunc`** | <code>(progress: { id: string; progress: number; }) => void</code> | - Callback receiving progress updates |
|
|
196
|
+
| Param | Type | Description |
|
|
197
|
+
| ------------------ | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------- |
|
|
198
|
+
| **`eventName`** | <code>'downloadProgress'</code> | - Must be 'downloadProgress' |
|
|
199
|
+
| **`listenerFunc`** | <code>(progress: { id: string; progress: number; bytesWritten?: number; bytesTotal?: number; }) => void</code> | - Callback receiving progress updates |
|
|
200
200
|
|
|
201
201
|
**Returns:** <code>Promise<<a href="#pluginlistenerhandle">PluginListenerHandle</a>></code>
|
|
202
202
|
|
|
@@ -284,14 +284,15 @@ Represents the current state and progress of a download task.
|
|
|
284
284
|
|
|
285
285
|
Configuration options for starting a download.
|
|
286
286
|
|
|
287
|
-
| Prop
|
|
288
|
-
|
|
|
289
|
-
| **`id`**
|
|
290
|
-
| **`url`**
|
|
291
|
-
| **`destination`**
|
|
292
|
-
| **`headers`**
|
|
293
|
-
| **`network`**
|
|
294
|
-
| **`priority`**
|
|
287
|
+
| Prop | Type | Description | Since |
|
|
288
|
+
| ------------------ | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
|
|
289
|
+
| **`id`** | <code>string</code> | Unique identifier for this download task | |
|
|
290
|
+
| **`url`** | <code>string</code> | URL of the file to download | |
|
|
291
|
+
| **`destination`** | <code>string</code> | Local file path where the download will be saved | |
|
|
292
|
+
| **`headers`** | <code>{ [key: string]: string; }</code> | Optional HTTP headers to include in the request | |
|
|
293
|
+
| **`network`** | <code>'cellular' \| 'wifi-only'</code> | Network type requirement for download | |
|
|
294
|
+
| **`priority`** | <code>'high' \| 'normal' \| 'low'</code> | Download priority level | |
|
|
295
|
+
| **`notification`** | <code>'completed' \| 'progress' \| 'hidden'</code> | Android DownloadManager notification visibility. - `'completed'` (default): show while downloading and keep the notification after completion. - `'progress'`: show while downloading, remove the notification when finished. - `'hidden'`: no system notification; use plugin progress events instead. Ignored on iOS and Web. | 8.2.0 |
|
|
295
296
|
|
|
296
297
|
|
|
297
298
|
#### PluginListenerHandle
|
package/android/src/main/java/ee/forgr/capacitor/plugin/downloader/CapacitorDownloaderPlugin.java
CHANGED
|
@@ -23,7 +23,7 @@ import java.util.Map;
|
|
|
23
23
|
@CapacitorPlugin(name = "CapacitorDownloader")
|
|
24
24
|
public class CapacitorDownloaderPlugin extends Plugin {
|
|
25
25
|
|
|
26
|
-
private final String pluginVersion = "8.
|
|
26
|
+
private final String pluginVersion = "8.3.0";
|
|
27
27
|
|
|
28
28
|
private DownloadManager downloadManager;
|
|
29
29
|
private final Map<String, Long> downloads = new HashMap<>();
|
|
@@ -76,7 +76,7 @@ public class CapacitorDownloaderPlugin extends Plugin {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url))
|
|
79
|
-
.setNotificationVisibility(
|
|
79
|
+
.setNotificationVisibility(DownloadNotificationVisibility.resolve(call.getString("notification")))
|
|
80
80
|
.setAllowedOverMetered(true)
|
|
81
81
|
.setAllowedOverRoaming(true);
|
|
82
82
|
|
|
@@ -100,7 +100,17 @@ public class CapacitorDownloaderPlugin extends Plugin {
|
|
|
100
100
|
request.setAllowedNetworkTypes(DownloadManager.Request.NETWORK_MOBILE | DownloadManager.Request.NETWORK_WIFI);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
long downloadId
|
|
103
|
+
long downloadId;
|
|
104
|
+
try {
|
|
105
|
+
downloadId = downloadManager.enqueue(request);
|
|
106
|
+
} catch (SecurityException e) {
|
|
107
|
+
if ("hidden".equals(call.getString("notification"))) {
|
|
108
|
+
call.reject("Hidden downloads require android.permission.DOWNLOAD_WITHOUT_NOTIFICATION in the app manifest", e);
|
|
109
|
+
} else {
|
|
110
|
+
call.reject("Download could not be enqueued due to missing permission", e);
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
104
114
|
downloads.put(id, downloadId);
|
|
105
115
|
|
|
106
116
|
JSObject result = new JSObject();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
package ee.forgr.capacitor.plugin.downloader;
|
|
2
|
+
|
|
3
|
+
import android.app.DownloadManager;
|
|
4
|
+
|
|
5
|
+
final class DownloadNotificationVisibility {
|
|
6
|
+
|
|
7
|
+
private DownloadNotificationVisibility() {}
|
|
8
|
+
|
|
9
|
+
static int resolve(String notification) {
|
|
10
|
+
if ("progress".equals(notification)) {
|
|
11
|
+
return DownloadManager.Request.VISIBILITY_VISIBLE;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if ("hidden".equals(notification)) {
|
|
15
|
+
return DownloadManager.Request.VISIBILITY_HIDDEN;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED;
|
|
19
|
+
}
|
|
20
|
+
}
|
package/dist/docs.json
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
"name": "example",
|
|
30
|
-
"text": "```typescript\nconst task = await Downloader.download({\n id: 'my-download',\n url: 'https://example.com/file.pdf',\n destination: 'downloads/file.pdf'\n});\n```"
|
|
30
|
+
"text": "```typescript\nconst task = await Downloader.download({\n id: 'my-download',\n url: 'https://example.com/file.pdf',\n destination: 'downloads/file.pdf',\n notification: 'hidden', // Android: no system notification; use downloadProgress events\n});\n```"
|
|
31
31
|
}
|
|
32
32
|
],
|
|
33
33
|
"docs": "Start a new download task.",
|
|
@@ -166,7 +166,7 @@
|
|
|
166
166
|
},
|
|
167
167
|
{
|
|
168
168
|
"name": "addListener",
|
|
169
|
-
"signature": "(eventName: 'downloadProgress', listenerFunc: (progress: { id: string; progress: number; }) => void) => Promise<PluginListenerHandle>",
|
|
169
|
+
"signature": "(eventName: 'downloadProgress', listenerFunc: (progress: { id: string; progress: number; bytesWritten?: number | undefined; bytesTotal?: number | undefined; }) => void) => Promise<PluginListenerHandle>",
|
|
170
170
|
"parameters": [
|
|
171
171
|
{
|
|
172
172
|
"name": "eventName",
|
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
{
|
|
177
177
|
"name": "listenerFunc",
|
|
178
178
|
"docs": "- Callback receiving progress updates",
|
|
179
|
-
"type": "(progress: { id: string; progress: number; }) => void"
|
|
179
|
+
"type": "(progress: { id: string; progress: number; bytesWritten?: number | undefined; bytesTotal?: number | undefined; }) => void"
|
|
180
180
|
}
|
|
181
181
|
],
|
|
182
182
|
"returns": "Promise<PluginListenerHandle>",
|
|
@@ -388,6 +388,18 @@
|
|
|
388
388
|
"docs": "Download priority level",
|
|
389
389
|
"complexTypes": [],
|
|
390
390
|
"type": "'high' | 'normal' | 'low' | undefined"
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
"name": "notification",
|
|
394
|
+
"tags": [
|
|
395
|
+
{
|
|
396
|
+
"text": "8.2.0",
|
|
397
|
+
"name": "since"
|
|
398
|
+
}
|
|
399
|
+
],
|
|
400
|
+
"docs": "Android DownloadManager notification visibility.\n\n- `'completed'` (default): show while downloading and keep the notification after completion.\n- `'progress'`: show while downloading, remove the notification when finished.\n- `'hidden'`: no system notification; use plugin progress events instead.\n\nIgnored on iOS and Web.",
|
|
401
|
+
"complexTypes": [],
|
|
402
|
+
"type": "'completed' | 'progress' | 'hidden' | undefined"
|
|
391
403
|
}
|
|
392
404
|
]
|
|
393
405
|
},
|
|
@@ -28,6 +28,18 @@ export interface DownloadOptions {
|
|
|
28
28
|
network?: 'cellular' | 'wifi-only';
|
|
29
29
|
/** Download priority level */
|
|
30
30
|
priority?: 'high' | 'normal' | 'low';
|
|
31
|
+
/**
|
|
32
|
+
* Android DownloadManager notification visibility.
|
|
33
|
+
*
|
|
34
|
+
* - `'completed'` (default): show while downloading and keep the notification after completion.
|
|
35
|
+
* - `'progress'`: show while downloading, remove the notification when finished.
|
|
36
|
+
* - `'hidden'`: no system notification; use plugin progress events instead.
|
|
37
|
+
*
|
|
38
|
+
* Ignored on iOS and Web.
|
|
39
|
+
*
|
|
40
|
+
* @since 8.2.0
|
|
41
|
+
*/
|
|
42
|
+
notification?: 'completed' | 'progress' | 'hidden';
|
|
31
43
|
}
|
|
32
44
|
/**
|
|
33
45
|
* Capacitor plugin for downloading files with background support.
|
|
@@ -44,7 +56,8 @@ export interface CapacitorDownloaderPlugin {
|
|
|
44
56
|
* const task = await Downloader.download({
|
|
45
57
|
* id: 'my-download',
|
|
46
58
|
* url: 'https://example.com/file.pdf',
|
|
47
|
-
* destination: 'downloads/file.pdf'
|
|
59
|
+
* destination: 'downloads/file.pdf',
|
|
60
|
+
* notification: 'hidden', // Android: no system notification; use downloadProgress events
|
|
48
61
|
* });
|
|
49
62
|
* ```
|
|
50
63
|
*/
|
|
@@ -117,6 +130,10 @@ export interface CapacitorDownloaderPlugin {
|
|
|
117
130
|
addListener(eventName: 'downloadProgress', listenerFunc: (progress: {
|
|
118
131
|
id: string;
|
|
119
132
|
progress: number;
|
|
133
|
+
/** Bytes written so far */
|
|
134
|
+
bytesWritten?: number;
|
|
135
|
+
/** Total bytes expected, or 0 when the server did not report a length */
|
|
136
|
+
bytesTotal?: number;
|
|
120
137
|
}) => void): Promise<PluginListenerHandle>;
|
|
121
138
|
/**
|
|
122
139
|
* Listen for download completion.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PluginListenerHandle } from '@capacitor/core';\n\n/**\n * Represents the current state and progress of a download task.\n */\nexport interface DownloadTask {\n /** Unique identifier for the download task */\n id: string;\n /** Download progress from 0 to 100 */\n progress: number;\n /** Current state of the download */\n state: 'PENDING' | 'RUNNING' | 'PAUSED' | 'DONE' | 'ERROR';\n}\n\n/**\n * Configuration options for starting a download.\n */\nexport interface DownloadOptions {\n /** Unique identifier for this download task */\n id: string;\n /** URL of the file to download */\n url: string;\n /** Local file path where the download will be saved */\n destination: string;\n /** Optional HTTP headers to include in the request */\n headers?: { [key: string]: string };\n /** Network type requirement for download */\n network?: 'cellular' | 'wifi-only';\n /** Download priority level */\n priority?: 'high' | 'normal' | 'low';\n}\n\n/**\n * Capacitor plugin for downloading files with background support.\n * Provides resumable downloads with progress tracking.\n */\nexport interface CapacitorDownloaderPlugin {\n /**\n * Start a new download task.\n *\n * @param options - Download configuration\n * @returns Promise with initial download task status\n * @example\n * ```typescript\n * const task = await Downloader.download({\n * id: 'my-download',\n * url: 'https://example.com/file.pdf',\n * destination: 'downloads/file.pdf'\n * });\n * ```\n */\n download(options: DownloadOptions): Promise<DownloadTask>;\n\n /**\n * Pause an active download.\n * Download can be resumed later from the same position.\n *\n * @param options - Options containing the download task ID\n * @returns Promise that resolves when paused\n */\n pause(options: { id: string }): Promise<void>;\n\n /**\n * Resume a paused download.\n * Continues from where it was paused.\n *\n * @param options - Options containing the download task ID\n * @returns Promise that resolves when resumed\n */\n resume(options: { id: string }): Promise<void>;\n\n /**\n * Stop and cancel a download permanently.\n * Downloaded data will be deleted.\n *\n * @param options - Options containing the download task ID\n * @returns Promise that resolves when stopped\n */\n stop(options: { id: string }): Promise<void>;\n\n /**\n * Check the current status of a download.\n *\n * @param options - Options containing the download task ID\n * @returns Promise with current download task status\n */\n checkStatus(options: { id: string }): Promise<DownloadTask>;\n\n /**\n * Get information about a downloaded file.\n *\n * @param options - Options containing the file path\n * @returns Promise with file size and MIME type\n */\n getFileInfo(options: { path: string }): Promise<{ size: number; type: string }>;\n\n /**\n * Listen for download progress updates.\n * Fired periodically as download progresses.\n *\n * @param eventName - Must be 'downloadProgress'\n * @param listenerFunc - Callback receiving progress updates\n * @returns Promise with listener handle for removal\n * @example\n * ```typescript\n * const listener = await Downloader.addListener('downloadProgress', (data) => {\n * console.log(`Download ${data.id}: ${data.progress}%`);\n * });\n * ```\n */\n addListener(\n eventName: 'downloadProgress',\n listenerFunc: (progress: {
|
|
1
|
+
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PluginListenerHandle } from '@capacitor/core';\n\n/**\n * Represents the current state and progress of a download task.\n */\nexport interface DownloadTask {\n /** Unique identifier for the download task */\n id: string;\n /** Download progress from 0 to 100 */\n progress: number;\n /** Current state of the download */\n state: 'PENDING' | 'RUNNING' | 'PAUSED' | 'DONE' | 'ERROR';\n}\n\n/**\n * Configuration options for starting a download.\n */\nexport interface DownloadOptions {\n /** Unique identifier for this download task */\n id: string;\n /** URL of the file to download */\n url: string;\n /** Local file path where the download will be saved */\n destination: string;\n /** Optional HTTP headers to include in the request */\n headers?: { [key: string]: string };\n /** Network type requirement for download */\n network?: 'cellular' | 'wifi-only';\n /** Download priority level */\n priority?: 'high' | 'normal' | 'low';\n /**\n * Android DownloadManager notification visibility.\n *\n * - `'completed'` (default): show while downloading and keep the notification after completion.\n * - `'progress'`: show while downloading, remove the notification when finished.\n * - `'hidden'`: no system notification; use plugin progress events instead.\n *\n * Ignored on iOS and Web.\n *\n * @since 8.2.0\n */\n notification?: 'completed' | 'progress' | 'hidden';\n}\n\n/**\n * Capacitor plugin for downloading files with background support.\n * Provides resumable downloads with progress tracking.\n */\nexport interface CapacitorDownloaderPlugin {\n /**\n * Start a new download task.\n *\n * @param options - Download configuration\n * @returns Promise with initial download task status\n * @example\n * ```typescript\n * const task = await Downloader.download({\n * id: 'my-download',\n * url: 'https://example.com/file.pdf',\n * destination: 'downloads/file.pdf',\n * notification: 'hidden', // Android: no system notification; use downloadProgress events\n * });\n * ```\n */\n download(options: DownloadOptions): Promise<DownloadTask>;\n\n /**\n * Pause an active download.\n * Download can be resumed later from the same position.\n *\n * @param options - Options containing the download task ID\n * @returns Promise that resolves when paused\n */\n pause(options: { id: string }): Promise<void>;\n\n /**\n * Resume a paused download.\n * Continues from where it was paused.\n *\n * @param options - Options containing the download task ID\n * @returns Promise that resolves when resumed\n */\n resume(options: { id: string }): Promise<void>;\n\n /**\n * Stop and cancel a download permanently.\n * Downloaded data will be deleted.\n *\n * @param options - Options containing the download task ID\n * @returns Promise that resolves when stopped\n */\n stop(options: { id: string }): Promise<void>;\n\n /**\n * Check the current status of a download.\n *\n * @param options - Options containing the download task ID\n * @returns Promise with current download task status\n */\n checkStatus(options: { id: string }): Promise<DownloadTask>;\n\n /**\n * Get information about a downloaded file.\n *\n * @param options - Options containing the file path\n * @returns Promise with file size and MIME type\n */\n getFileInfo(options: { path: string }): Promise<{ size: number; type: string }>;\n\n /**\n * Listen for download progress updates.\n * Fired periodically as download progresses.\n *\n * @param eventName - Must be 'downloadProgress'\n * @param listenerFunc - Callback receiving progress updates\n * @returns Promise with listener handle for removal\n * @example\n * ```typescript\n * const listener = await Downloader.addListener('downloadProgress', (data) => {\n * console.log(`Download ${data.id}: ${data.progress}%`);\n * });\n * ```\n */\n addListener(\n eventName: 'downloadProgress',\n listenerFunc: (progress: {\n id: string;\n progress: number;\n /** Bytes written so far */\n bytesWritten?: number;\n /** Total bytes expected, or 0 when the server did not report a length */\n bytesTotal?: number;\n }) => void,\n ): Promise<PluginListenerHandle>;\n\n /**\n * Listen for download completion.\n * Fired when a download finishes successfully.\n *\n * @param eventName - Must be 'downloadCompleted'\n * @param listenerFunc - Callback receiving completion notification\n * @returns Promise with listener handle for removal\n */\n addListener(\n eventName: 'downloadCompleted',\n listenerFunc: (result: { id: string }) => void,\n ): Promise<PluginListenerHandle>;\n\n /**\n * Listen for download failures.\n * Fired when a download encounters an error.\n *\n * @param eventName - Must be 'downloadFailed'\n * @param listenerFunc - Callback receiving error information\n * @returns Promise with listener handle for removal\n */\n addListener(\n eventName: 'downloadFailed',\n listenerFunc: (error: { id: string; error: string }) => void,\n ): Promise<PluginListenerHandle>;\n\n /**\n * Remove all event listeners.\n * Cleanup method to prevent memory leaks.\n *\n * @returns Promise that resolves when all listeners removed\n */\n removeAllListeners(): Promise<void>;\n\n /**\n * Get the plugin version number.\n *\n * @returns Promise with version string\n */\n getPluginVersion(): Promise<{ version: string }>;\n}\n"]}
|
|
@@ -8,7 +8,7 @@ import Capacitor
|
|
|
8
8
|
|
|
9
9
|
@objc(CapacitorDownloaderPlugin)
|
|
10
10
|
public class CapacitorDownloaderPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
11
|
-
private let pluginVersion: String = "8.
|
|
11
|
+
private let pluginVersion: String = "8.3.0"
|
|
12
12
|
public let identifier = "CapacitorDownloaderPlugin"
|
|
13
13
|
public let jsName = "CapacitorDownloader"
|
|
14
14
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
@@ -56,15 +56,38 @@ public class CapacitorDownloaderPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
56
56
|
return tasks.first(where: { $0.value == task })?.key
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
private func documentsDirectoryURL() -> URL? {
|
|
60
|
+
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private func destinationURL(for id: String, storedDestination: String?) -> URL? {
|
|
64
|
+
guard let documentsDirectory = documentsDirectoryURL() else {
|
|
65
|
+
return nil
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return DownloadDestinationResolver.resolveDestinationURL(
|
|
69
|
+
destination: storedDestination,
|
|
70
|
+
id: id,
|
|
71
|
+
documentsDirectory: documentsDirectory
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private func prepareDestinationDirectory(for destinationURL: URL) throws {
|
|
76
|
+
let directoryURL = destinationURL.deletingLastPathComponent()
|
|
77
|
+
try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true)
|
|
78
|
+
}
|
|
79
|
+
|
|
59
80
|
@objc func download(_ call: CAPPluginCall) {
|
|
60
81
|
guard let id = call.getString("id"),
|
|
61
82
|
let urlString = call.getString("url"),
|
|
62
|
-
let destination = call.getString("destination"),
|
|
63
83
|
let url = URL(string: urlString) else {
|
|
64
84
|
call.reject("Invalid parameters")
|
|
65
85
|
return
|
|
66
86
|
}
|
|
67
87
|
|
|
88
|
+
let destination = call.getString("destination") ?? ""
|
|
89
|
+
DownloadDestinationStore.shared.setDestination(destination, for: id)
|
|
90
|
+
|
|
68
91
|
var request = URLRequest(url: url)
|
|
69
92
|
if let headers = call.getObject("headers") as? [String: String] {
|
|
70
93
|
for (key, value) in headers {
|
|
@@ -115,6 +138,7 @@ public class CapacitorDownloaderPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
115
138
|
|
|
116
139
|
task.cancel()
|
|
117
140
|
removeTask(for: id)
|
|
141
|
+
DownloadDestinationStore.shared.removeDestination(for: id)
|
|
118
142
|
call.resolve()
|
|
119
143
|
}
|
|
120
144
|
|
|
@@ -166,16 +190,39 @@ public class CapacitorDownloaderPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
|
166
190
|
"type": type
|
|
167
191
|
])
|
|
168
192
|
}
|
|
193
|
+
|
|
194
|
+
@objc func getPluginVersion(_ call: CAPPluginCall) {
|
|
195
|
+
call.resolve(["version": self.pluginVersion])
|
|
196
|
+
}
|
|
169
197
|
}
|
|
170
198
|
|
|
171
199
|
extension CapacitorDownloaderPlugin: URLSessionDownloadDelegate {
|
|
172
200
|
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
|
|
173
|
-
guard let id = idForTask(downloadTask)
|
|
174
|
-
|
|
201
|
+
guard let id = idForTask(downloadTask) else {
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let storedDestination = DownloadDestinationStore.shared.takeDestination(for: id)
|
|
206
|
+
guard let destinationURL = destinationURL(for: id, storedDestination: storedDestination) else {
|
|
207
|
+
notifyListeners("downloadFailed", data: ["id": id, "error": "Unable to resolve destination"])
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if let httpResponse = downloadTask.response as? HTTPURLResponse,
|
|
212
|
+
!DownloadHTTPValidator.isSuccessfulStatusCode(httpResponse.statusCode) {
|
|
213
|
+
try? FileManager.default.removeItem(at: location)
|
|
214
|
+
notifyListeners(
|
|
215
|
+
"downloadFailed",
|
|
216
|
+
data: ["id": id, "error": "HTTP \(httpResponse.statusCode)"]
|
|
217
|
+
)
|
|
175
218
|
return
|
|
176
219
|
}
|
|
177
220
|
|
|
178
221
|
do {
|
|
222
|
+
try prepareDestinationDirectory(for: destinationURL)
|
|
223
|
+
if FileManager.default.fileExists(atPath: destinationURL.path) {
|
|
224
|
+
try FileManager.default.removeItem(at: destinationURL)
|
|
225
|
+
}
|
|
179
226
|
try FileManager.default.moveItem(at: location, to: destinationURL)
|
|
180
227
|
notifyListeners("downloadCompleted", data: ["id": id])
|
|
181
228
|
} catch {
|
|
@@ -191,22 +238,36 @@ extension CapacitorDownloaderPlugin: URLSessionDownloadDelegate {
|
|
|
191
238
|
|
|
192
239
|
removeTask(for: id)
|
|
193
240
|
|
|
241
|
+
if error != nil {
|
|
242
|
+
_ = DownloadDestinationStore.shared.takeDestination(for: id)
|
|
243
|
+
}
|
|
244
|
+
|
|
194
245
|
if let error = error {
|
|
195
246
|
notifyListeners("downloadFailed", data: ["id": id, "error": error.localizedDescription])
|
|
196
247
|
}
|
|
197
248
|
}
|
|
198
249
|
|
|
199
|
-
public func urlSession(
|
|
250
|
+
public func urlSession(
|
|
251
|
+
_ session: URLSession,
|
|
252
|
+
downloadTask: URLSessionDownloadTask,
|
|
253
|
+
didWriteData bytesWritten: Int64,
|
|
254
|
+
totalBytesWritten: Int64,
|
|
255
|
+
totalBytesExpectedToWrite: Int64
|
|
256
|
+
) {
|
|
200
257
|
guard let id = idForTask(downloadTask) else {
|
|
201
258
|
return
|
|
202
259
|
}
|
|
203
260
|
|
|
204
|
-
let
|
|
205
|
-
|
|
261
|
+
let bytesTotal = totalBytesExpectedToWrite > 0 ? totalBytesExpectedToWrite : 0
|
|
262
|
+
let progress = bytesTotal > 0 ? Float(totalBytesWritten) / Float(bytesTotal) : 0
|
|
263
|
+
notifyListeners(
|
|
264
|
+
"downloadProgress",
|
|
265
|
+
data: [
|
|
266
|
+
"id": id,
|
|
267
|
+
"progress": progress,
|
|
268
|
+
"bytesWritten": totalBytesWritten,
|
|
269
|
+
"bytesTotal": bytesTotal
|
|
270
|
+
]
|
|
271
|
+
)
|
|
206
272
|
}
|
|
207
|
-
|
|
208
|
-
@objc func getPluginVersion(_ call: CAPPluginCall) {
|
|
209
|
-
call.resolve(["version": self.pluginVersion])
|
|
210
|
-
}
|
|
211
|
-
|
|
212
273
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
enum DownloadDestinationResolver {
|
|
4
|
+
static func resolveDestinationURL(
|
|
5
|
+
destination: String?,
|
|
6
|
+
id: String,
|
|
7
|
+
documentsDirectory: URL
|
|
8
|
+
) -> URL {
|
|
9
|
+
let trimmedDestination = destination?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
10
|
+
guard let destination = trimmedDestination, !destination.isEmpty else {
|
|
11
|
+
return documentsDirectory.appendingPathComponent(id)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if destination.hasPrefix("file://"), let url = URL(string: destination) {
|
|
15
|
+
return url
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if destination.hasPrefix("/") {
|
|
19
|
+
return URL(fileURLWithPath: destination)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return documentsDirectory.appendingPathComponent(destination)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
final class DownloadDestinationStore {
|
|
4
|
+
static let shared = DownloadDestinationStore()
|
|
5
|
+
|
|
6
|
+
static let userDefaultsKey = "CapacitorDownloaderDestinations"
|
|
7
|
+
|
|
8
|
+
private let defaults: UserDefaults
|
|
9
|
+
|
|
10
|
+
init(defaults: UserDefaults = .standard) {
|
|
11
|
+
self.defaults = defaults
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
func setDestination(_ destination: String, for id: String) {
|
|
15
|
+
var map = destinationsMap()
|
|
16
|
+
map[id] = destination
|
|
17
|
+
defaults.set(map, forKey: Self.userDefaultsKey)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func takeDestination(for id: String) -> String? {
|
|
21
|
+
var map = destinationsMap()
|
|
22
|
+
let destination = map.removeValue(forKey: id)
|
|
23
|
+
defaults.set(map, forKey: Self.userDefaultsKey)
|
|
24
|
+
return destination
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func removeDestination(for id: String) {
|
|
28
|
+
var map = destinationsMap()
|
|
29
|
+
map.removeValue(forKey: id)
|
|
30
|
+
defaults.set(map, forKey: Self.userDefaultsKey)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private func destinationsMap() -> [String: String] {
|
|
34
|
+
defaults.dictionary(forKey: Self.userDefaultsKey) as? [String: String] ?? [:]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -1,15 +1,102 @@
|
|
|
1
1
|
import XCTest
|
|
2
2
|
@testable import CapacitorDownloaderPlugin
|
|
3
3
|
|
|
4
|
-
class
|
|
5
|
-
|
|
6
|
-
// This is an example of a functional test case for a plugin.
|
|
7
|
-
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
|
4
|
+
final class DownloadDestinationResolverTests: XCTestCase {
|
|
5
|
+
private let documentsDirectory = URL(fileURLWithPath: "/var/mobile/Documents")
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
let
|
|
11
|
-
|
|
7
|
+
func testEmptyDestinationFallsBackToDocumentsPlusId() {
|
|
8
|
+
let url = DownloadDestinationResolver.resolveDestinationURL(
|
|
9
|
+
destination: nil,
|
|
10
|
+
id: "download-1",
|
|
11
|
+
documentsDirectory: documentsDirectory
|
|
12
|
+
)
|
|
12
13
|
|
|
13
|
-
XCTAssertEqual(
|
|
14
|
+
XCTAssertEqual(url, documentsDirectory.appendingPathComponent("download-1"))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
func testBlankDestinationFallsBackToDocumentsPlusId() {
|
|
18
|
+
let url = DownloadDestinationResolver.resolveDestinationURL(
|
|
19
|
+
destination: " ",
|
|
20
|
+
id: "download-1",
|
|
21
|
+
documentsDirectory: documentsDirectory
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
XCTAssertEqual(url, documentsDirectory.appendingPathComponent("download-1"))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func testRelativeDestinationUsesDocumentsDirectory() {
|
|
28
|
+
let url = DownloadDestinationResolver.resolveDestinationURL(
|
|
29
|
+
destination: "downloads/sample.zip",
|
|
30
|
+
id: "download-1",
|
|
31
|
+
documentsDirectory: documentsDirectory
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
XCTAssertEqual(url, documentsDirectory.appendingPathComponent("downloads/sample.zip"))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func testAbsolutePathUsesFileURL() {
|
|
38
|
+
let url = DownloadDestinationResolver.resolveDestinationURL(
|
|
39
|
+
destination: "/tmp/custom-file.bin",
|
|
40
|
+
id: "download-1",
|
|
41
|
+
documentsDirectory: documentsDirectory
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
XCTAssertEqual(url, URL(fileURLWithPath: "/tmp/custom-file.bin"))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
func testFileURLDestinationIsUsedDirectly() {
|
|
48
|
+
let url = DownloadDestinationResolver.resolveDestinationURL(
|
|
49
|
+
destination: "file:///tmp/custom-file.bin",
|
|
50
|
+
id: "download-1",
|
|
51
|
+
documentsDirectory: documentsDirectory
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
XCTAssertEqual(url, URL(string: "file:///tmp/custom-file.bin"))
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
final class DownloadDestinationStoreTests: XCTestCase {
|
|
59
|
+
private var defaults: UserDefaults!
|
|
60
|
+
private var store: DownloadDestinationStore!
|
|
61
|
+
|
|
62
|
+
override func setUp() {
|
|
63
|
+
super.setUp()
|
|
64
|
+
defaults = UserDefaults(suiteName: "CapacitorDownloaderPluginTests")!
|
|
65
|
+
defaults.removePersistentDomain(forName: "CapacitorDownloaderPluginTests")
|
|
66
|
+
store = DownloadDestinationStore(defaults: defaults)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
override func tearDown() {
|
|
70
|
+
defaults.removePersistentDomain(forName: "CapacitorDownloaderPluginTests")
|
|
71
|
+
super.tearDown()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
func testSetAndTakeDestination() {
|
|
75
|
+
store.setDestination("downloads/file.zip", for: "task-1")
|
|
76
|
+
|
|
77
|
+
XCTAssertEqual(store.takeDestination(for: "task-1"), "downloads/file.zip")
|
|
78
|
+
XCTAssertNil(store.takeDestination(for: "task-1"))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func testRemoveDestination() {
|
|
82
|
+
store.setDestination("downloads/file.zip", for: "task-1")
|
|
83
|
+
|
|
84
|
+
store.removeDestination(for: "task-1")
|
|
85
|
+
|
|
86
|
+
XCTAssertNil(store.takeDestination(for: "task-1"))
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
final class DownloadHTTPValidatorTests: XCTestCase {
|
|
91
|
+
func testSuccessfulStatusCodes() {
|
|
92
|
+
XCTAssertTrue(DownloadHTTPValidator.isSuccessfulStatusCode(200))
|
|
93
|
+
XCTAssertTrue(DownloadHTTPValidator.isSuccessfulStatusCode(204))
|
|
94
|
+
XCTAssertTrue(DownloadHTTPValidator.isSuccessfulStatusCode(299))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
func testNonSuccessfulStatusCodesAreRejected() {
|
|
98
|
+
XCTAssertFalse(DownloadHTTPValidator.isSuccessfulStatusCode(199))
|
|
99
|
+
XCTAssertFalse(DownloadHTTPValidator.isSuccessfulStatusCode(404))
|
|
100
|
+
XCTAssertFalse(DownloadHTTPValidator.isSuccessfulStatusCode(500))
|
|
14
101
|
}
|
|
15
102
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capgo/capacitor-downloader",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.3.0",
|
|
4
4
|
"description": "Download file in background or foreground",
|
|
5
5
|
"main": "dist/plugin.cjs.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -36,17 +36,17 @@
|
|
|
36
36
|
"download file in foreground"
|
|
37
37
|
],
|
|
38
38
|
"scripts": {
|
|
39
|
-
"verify": "
|
|
40
|
-
"verify:ios": "
|
|
39
|
+
"verify": "bun run verify:ios && bun run verify:android && bun run verify:web",
|
|
40
|
+
"verify:ios": "bun scripts/verify-ios.mjs",
|
|
41
41
|
"verify:android": "cd android && ./gradlew clean build test && cd ..",
|
|
42
|
-
"verify:web": "
|
|
43
|
-
"lint": "
|
|
44
|
-
"fmt": "
|
|
42
|
+
"verify:web": "bun run build",
|
|
43
|
+
"lint": "bun run eslint && bun run prettier -- --check && bun run swiftlint -- lint",
|
|
44
|
+
"fmt": "bun run eslint -- --fix && bun run prettier -- --write && bun run swiftlint -- --fix --format",
|
|
45
45
|
"eslint": "eslint . --ext .ts",
|
|
46
46
|
"prettier": "prettier-pretty-check \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
|
|
47
47
|
"swiftlint": "node-swiftlint",
|
|
48
48
|
"docgen": "docgen --api CapacitorDownloaderPlugin --output-readme README.md --output-json dist/docs.json",
|
|
49
|
-
"build": "
|
|
49
|
+
"build": "bun run clean && bun run docgen && tsc && rollup -c rollup.config.mjs",
|
|
50
50
|
"clean": "rimraf ./dist",
|
|
51
51
|
"watch": "tsc --watch",
|
|
52
52
|
"prepublishOnly": "bun run build",
|