@simplysm/capacitor-plugin-usb-storage 13.0.69 → 13.0.71
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 +9 -254
- package/dist/UsbStorage.d.ts +19 -19
- package/dist/UsbStorage.js +16 -16
- package/dist/web/UsbStorageWeb.d.ts +3 -3
- package/dist/web/UsbStorageWeb.js +3 -3
- package/package.json +5 -4
- package/src/IUsbStoragePlugin.ts +25 -0
- package/src/UsbStorage.ts +77 -0
- package/src/index.ts +3 -0
- package/src/web/IndexedDbStore.ts +88 -0
- package/src/web/UsbStorageWeb.ts +104 -0
- package/src/web/VirtualUsbStorage.ts +107 -0
package/README.md
CHANGED
|
@@ -1,267 +1,22 @@
|
|
|
1
1
|
# @simplysm/capacitor-plugin-usb-storage
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Simplysm Package - Capacitor USB Storage Plugin
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
|
-
```bash
|
|
8
7
|
pnpm add @simplysm/capacitor-plugin-usb-storage
|
|
9
|
-
npx cap sync
|
|
10
|
-
```
|
|
11
8
|
|
|
12
|
-
|
|
9
|
+
**Peer Dependencies:** `@capacitor/core ^7.4.4`
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
|--------|-----------|-----------|
|
|
16
|
-
| Android | Yes | USB Mass Storage access through libaums 0.9.1 |
|
|
17
|
-
| Web | Yes (emulation) | IndexedDB-based virtual USB storage |
|
|
18
|
-
| iOS | No | -- |
|
|
11
|
+
## Source Index
|
|
19
12
|
|
|
20
|
-
###
|
|
13
|
+
### USB Storage
|
|
21
14
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
## Main Modules
|
|
27
|
-
|
|
28
|
-
### UsbStorage (Static Class)
|
|
29
|
-
|
|
30
|
-
The main entry point of the plugin. All methods are static and operate asynchronously.
|
|
31
|
-
|
|
32
|
-
```typescript
|
|
33
|
-
import { UsbStorage } from "@simplysm/capacitor-plugin-usb-storage";
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
| Method | Return Type | Description |
|
|
37
|
-
|--------|-----------|------|
|
|
38
|
-
| `getDevices()` | `Promise<IUsbDeviceInfo[]>` | Retrieve list of connected USB devices |
|
|
39
|
-
| `requestPermission(filter)` | `Promise<boolean>` | Request USB device access permission |
|
|
40
|
-
| `hasPermission(filter)` | `Promise<boolean>` | Check if USB device access permission is granted |
|
|
41
|
-
| `readdir(filter, dirPath)` | `Promise<IUsbFileInfo[]>` | Read list of files/folders in directory |
|
|
42
|
-
| `read(filter, filePath)` | `Promise<Bytes \| undefined>` | Read file contents as binary |
|
|
43
|
-
|
|
44
|
-
### IUsbStoragePlugin (Raw Plugin Interface)
|
|
45
|
-
|
|
46
|
-
The low-level Capacitor plugin interface. Most users should use the `UsbStorage` static class instead.
|
|
47
|
-
This interface is useful for advanced scenarios such as creating custom plugin implementations.
|
|
48
|
-
|
|
49
|
-
```typescript
|
|
50
|
-
import type { IUsbStoragePlugin } from "@simplysm/capacitor-plugin-usb-storage";
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
| Method | Return Type | Description |
|
|
54
|
-
|--------|-----------|------|
|
|
55
|
-
| `getDevices()` | `Promise<{ devices: IUsbDeviceInfo[] }>` | Get connected USB devices (raw) |
|
|
56
|
-
| `requestPermission(options)` | `Promise<{ granted: boolean }>` | Request USB permission (raw) |
|
|
57
|
-
| `hasPermission(options)` | `Promise<{ granted: boolean }>` | Check USB permission (raw) |
|
|
58
|
-
| `readdir(options)` | `Promise<{ files: IUsbFileInfo[] }>` | List directory contents (raw) |
|
|
59
|
-
| `read(options)` | `Promise<{ data: string \| null }>` | Read file as Base64 string (raw) |
|
|
60
|
-
|
|
61
|
-
### Interfaces
|
|
62
|
-
|
|
63
|
-
```typescript
|
|
64
|
-
import type { IUsbDeviceInfo, IUsbDeviceFilter, IUsbFileInfo } from "@simplysm/capacitor-plugin-usb-storage";
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
#### IUsbDeviceInfo
|
|
68
|
-
|
|
69
|
-
Represents USB device information.
|
|
70
|
-
|
|
71
|
-
| Property | Type | Description |
|
|
72
|
-
|------|------|------|
|
|
73
|
-
| `deviceName` | `string` | Device name (system path) |
|
|
74
|
-
| `manufacturerName` | `string` | Manufacturer name |
|
|
75
|
-
| `productName` | `string` | Product name |
|
|
76
|
-
| `vendorId` | `number` | USB Vendor ID |
|
|
77
|
-
| `productId` | `number` | USB Product ID |
|
|
78
|
-
|
|
79
|
-
#### IUsbDeviceFilter
|
|
80
|
-
|
|
81
|
-
A filter for identifying specific USB devices. Specifies the device using the combination of `vendorId` and `productId`.
|
|
82
|
-
|
|
83
|
-
| Property | Type | Description |
|
|
84
|
-
|------|------|------|
|
|
85
|
-
| `vendorId` | `number` | USB Vendor ID |
|
|
86
|
-
| `productId` | `number` | USB Product ID |
|
|
87
|
-
|
|
88
|
-
#### IUsbFileInfo
|
|
89
|
-
|
|
90
|
-
Represents file or directory information.
|
|
91
|
-
|
|
92
|
-
| Property | Type | Description |
|
|
93
|
-
|------|------|------|
|
|
94
|
-
| `name` | `string` | File/directory name |
|
|
95
|
-
| `isDirectory` | `boolean` | Whether it's a directory |
|
|
96
|
-
|
|
97
|
-
## Usage Examples
|
|
98
|
-
|
|
99
|
-
### List Devices and Request Permission
|
|
100
|
-
|
|
101
|
-
```typescript
|
|
102
|
-
import { UsbStorage } from "@simplysm/capacitor-plugin-usb-storage";
|
|
103
|
-
|
|
104
|
-
// Retrieve connected USB device list
|
|
105
|
-
const devices = await UsbStorage.getDevices();
|
|
106
|
-
console.log("Connected devices:", devices);
|
|
107
|
-
|
|
108
|
-
if (devices.length > 0) {
|
|
109
|
-
const device = devices[0];
|
|
110
|
-
const filter = { vendorId: device.vendorId, productId: device.productId };
|
|
111
|
-
|
|
112
|
-
// Check permission
|
|
113
|
-
const hasPerm = await UsbStorage.hasPermission(filter);
|
|
114
|
-
if (!hasPerm) {
|
|
115
|
-
// Request permission (displays system dialog on Android)
|
|
116
|
-
const granted = await UsbStorage.requestPermission(filter);
|
|
117
|
-
if (!granted) {
|
|
118
|
-
console.log("USB device access permission was denied.");
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
### Read Directory Contents
|
|
126
|
-
|
|
127
|
-
```typescript
|
|
128
|
-
import { UsbStorage } from "@simplysm/capacitor-plugin-usb-storage";
|
|
129
|
-
|
|
130
|
-
const filter = { vendorId: 1234, productId: 5678 };
|
|
131
|
-
|
|
132
|
-
// Read root directory
|
|
133
|
-
const rootFiles = await UsbStorage.readdir(filter, "/");
|
|
134
|
-
for (const file of rootFiles) {
|
|
135
|
-
const type = file.isDirectory ? "[DIR]" : "[FILE]";
|
|
136
|
-
console.log(`${type} ${file.name}`);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Read subdirectory
|
|
140
|
-
const subFiles = await UsbStorage.readdir(filter, "/Documents");
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
### Read File
|
|
144
|
-
|
|
145
|
-
```typescript
|
|
146
|
-
import { UsbStorage } from "@simplysm/capacitor-plugin-usb-storage";
|
|
147
|
-
|
|
148
|
-
const filter = { vendorId: 1234, productId: 5678 };
|
|
149
|
-
|
|
150
|
-
// Read file as binary (Bytes)
|
|
151
|
-
const data = await UsbStorage.read(filter, "/data/config.json");
|
|
152
|
-
if (data != null) {
|
|
153
|
-
// Convert Bytes to string
|
|
154
|
-
const text = new TextDecoder().decode(data);
|
|
155
|
-
console.log("File contents:", text);
|
|
156
|
-
}
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
### Complete Flow Example
|
|
160
|
-
|
|
161
|
-
```typescript
|
|
162
|
-
import { UsbStorage } from "@simplysm/capacitor-plugin-usb-storage";
|
|
163
|
-
import type { IUsbDeviceFilter } from "@simplysm/capacitor-plugin-usb-storage";
|
|
164
|
-
|
|
165
|
-
async function readUsbFile(filePath: string): Promise<string | undefined> {
|
|
166
|
-
// 1. Search for device
|
|
167
|
-
const devices = await UsbStorage.getDevices();
|
|
168
|
-
if (devices.length === 0) {
|
|
169
|
-
throw new Error("No USB device is connected.");
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const device = devices[0];
|
|
173
|
-
const filter: IUsbDeviceFilter = {
|
|
174
|
-
vendorId: device.vendorId,
|
|
175
|
-
productId: device.productId,
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
// 2. Secure permission
|
|
179
|
-
const hasPerm = await UsbStorage.hasPermission(filter);
|
|
180
|
-
if (!hasPerm) {
|
|
181
|
-
const granted = await UsbStorage.requestPermission(filter);
|
|
182
|
-
if (!granted) {
|
|
183
|
-
throw new Error("USB device access permission is required.");
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// 3. Read file
|
|
188
|
-
const data = await UsbStorage.read(filter, filePath);
|
|
189
|
-
if (data == null) {
|
|
190
|
-
return undefined;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return new TextDecoder().decode(data);
|
|
194
|
-
}
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
## Web Emulation (Development/Testing)
|
|
198
|
-
|
|
199
|
-
In web environments, the `UsbStorageWeb` class is automatically used, providing an IndexedDB-based virtual USB storage. Permission requests are always processed as approved.
|
|
200
|
-
|
|
201
|
-
`UsbStorageWeb` provides methods for adding virtual devices and files for development and testing purposes.
|
|
202
|
-
|
|
203
|
-
> **Note**: `UsbStorageWeb` is not re-exported from the main package entry point.
|
|
204
|
-
> Import it via a deep path as shown below.
|
|
205
|
-
|
|
206
|
-
```typescript
|
|
207
|
-
import { UsbStorageWeb } from "@simplysm/capacitor-plugin-usb-storage/dist/web/UsbStorageWeb";
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
| Method | Description |
|
|
211
|
-
|--------|------|
|
|
212
|
-
| `addVirtualDevice(device)` | Register a virtual USB device |
|
|
213
|
-
| `addVirtualFile(filter, filePath, data)` | Add a file to virtual device (parent directories created automatically) |
|
|
214
|
-
| `addVirtualDirectory(filter, dirPath)` | Add a directory to virtual device |
|
|
215
|
-
|
|
216
|
-
### Web Emulation Usage Example
|
|
217
|
-
|
|
218
|
-
```typescript
|
|
219
|
-
import { UsbStorageWeb } from "@simplysm/capacitor-plugin-usb-storage/dist/web/UsbStorageWeb";
|
|
220
|
-
|
|
221
|
-
const web = new UsbStorageWeb();
|
|
222
|
-
|
|
223
|
-
// Add virtual device
|
|
224
|
-
await web.addVirtualDevice({
|
|
225
|
-
vendorId: 1234,
|
|
226
|
-
productId: 5678,
|
|
227
|
-
deviceName: "Virtual USB",
|
|
228
|
-
manufacturerName: "Test Manufacturer",
|
|
229
|
-
productName: "Test USB Drive",
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
const filter = { vendorId: 1234, productId: 5678 };
|
|
233
|
-
|
|
234
|
-
// Add virtual file
|
|
235
|
-
const content = new TextEncoder().encode("Hello, USB!");
|
|
236
|
-
await web.addVirtualFile(filter, "/test/hello.txt", content);
|
|
237
|
-
|
|
238
|
-
// Add virtual directory
|
|
239
|
-
await web.addVirtualDirectory(filter, "/test/subdir");
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
## Android Native Implementation Details
|
|
243
|
-
|
|
244
|
-
The Android native layer uses the `libaums` library to directly handle the USB Mass Storage protocol. Key operations are as follows.
|
|
245
|
-
|
|
246
|
-
- **Device Search**: Uses `UsbMassStorageDevice.getMassStorageDevices()` to query connected USB Mass Storage devices.
|
|
247
|
-
- **Permission Management**: Requests and verifies USB device access permission through Android's `UsbManager`. On Android 12 (API 31) and above, it uses `PendingIntent.FLAG_MUTABLE`, and on Android 13 (API 33) and above, it applies the `RECEIVER_NOT_EXPORTED` flag.
|
|
248
|
-
- **File System Access**: Mounts the file system of the first partition to perform directory navigation and file reading.
|
|
249
|
-
- **Data Transfer**: File data is Base64-encoded for transmission to the JavaScript layer.
|
|
250
|
-
|
|
251
|
-
## Dependencies
|
|
252
|
-
|
|
253
|
-
### Peer Dependencies
|
|
254
|
-
|
|
255
|
-
| Package | Version |
|
|
256
|
-
|--------|------|
|
|
257
|
-
| `@capacitor/core` | `^7.4.4` |
|
|
258
|
-
|
|
259
|
-
### Internal Dependencies
|
|
260
|
-
|
|
261
|
-
| Package | Description |
|
|
262
|
-
|--------|------|
|
|
263
|
-
| `@simplysm/core-common` | Common utilities such as Base64 conversion, `Bytes` type |
|
|
15
|
+
| Source | Exports | Description | Test |
|
|
16
|
+
|--------|---------|-------------|------|
|
|
17
|
+
| `src/IUsbStoragePlugin.ts` | `IUsbDeviceInfo`, `IUsbDeviceFilter`, `IUsbFileInfo`, `IUsbStoragePlugin` | Interfaces for USB device info, file info, filters, and the native plugin contract | - |
|
|
18
|
+
| `src/UsbStorage.ts` | `UsbStorage` | Static class to enumerate USB devices, manage permissions, and read files | - |
|
|
264
19
|
|
|
265
20
|
## License
|
|
266
21
|
|
|
267
|
-
|
|
22
|
+
Apache-2.0
|
package/dist/UsbStorage.d.ts
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
1
|
import type { IUsbDeviceFilter, IUsbDeviceInfo, IUsbFileInfo } from "./IUsbStoragePlugin";
|
|
2
2
|
import type { Bytes } from "@simplysm/core-common";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* - Android:
|
|
6
|
-
* - Browser: IndexedDB
|
|
4
|
+
* Plugin for interacting with USB storage devices
|
|
5
|
+
* - Android: USB Mass Storage access via libaums library
|
|
6
|
+
* - Browser: IndexedDB-based virtual USB storage emulation
|
|
7
7
|
*/
|
|
8
8
|
export declare abstract class UsbStorage {
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
* @returns
|
|
10
|
+
* Get list of connected USB devices
|
|
11
|
+
* @returns Array of connected USB device info
|
|
12
12
|
*/
|
|
13
13
|
static getDevices(): Promise<IUsbDeviceInfo[]>;
|
|
14
14
|
/**
|
|
15
|
-
* USB
|
|
16
|
-
* @param filter
|
|
17
|
-
* @returns
|
|
15
|
+
* Request USB device access permission
|
|
16
|
+
* @param filter vendorId and productId of the USB device to request permission for
|
|
17
|
+
* @returns Whether permission was granted
|
|
18
18
|
*/
|
|
19
19
|
static requestPermission(filter: IUsbDeviceFilter): Promise<boolean>;
|
|
20
20
|
/**
|
|
21
|
-
* USB
|
|
22
|
-
* @param filter
|
|
23
|
-
* @returns
|
|
21
|
+
* Check if USB device access permission is granted
|
|
22
|
+
* @param filter vendorId and productId of the USB device to check permission for
|
|
23
|
+
* @returns Whether permission is held
|
|
24
24
|
*/
|
|
25
25
|
static hasPermission(filter: IUsbDeviceFilter): Promise<boolean>;
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
28
|
-
* @param filter
|
|
29
|
-
* @param dirPath
|
|
30
|
-
* @returns
|
|
27
|
+
* Read directory contents from USB storage device
|
|
28
|
+
* @param filter vendorId and productId of the target USB device
|
|
29
|
+
* @param dirPath Directory path to read
|
|
30
|
+
* @returns Array of file/folder info in the directory
|
|
31
31
|
*/
|
|
32
32
|
static readdir(filter: IUsbDeviceFilter, dirPath: string): Promise<IUsbFileInfo[]>;
|
|
33
33
|
/**
|
|
34
|
-
* USB
|
|
35
|
-
* @param filter
|
|
36
|
-
* @param filePath
|
|
37
|
-
* @returns
|
|
34
|
+
* Read a file from USB storage device
|
|
35
|
+
* @param filter vendorId and productId of the target USB device
|
|
36
|
+
* @param filePath File path to read
|
|
37
|
+
* @returns Bytes containing file data, or undefined
|
|
38
38
|
*/
|
|
39
39
|
static read(filter: IUsbDeviceFilter, filePath: string): Promise<Bytes | undefined>;
|
|
40
40
|
}
|
package/dist/UsbStorage.js
CHANGED
|
@@ -8,46 +8,46 @@ const UsbStoragePlugin = registerPlugin("UsbStorage", {
|
|
|
8
8
|
});
|
|
9
9
|
class UsbStorage {
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
* @returns
|
|
11
|
+
* Get list of connected USB devices
|
|
12
|
+
* @returns Array of connected USB device info
|
|
13
13
|
*/
|
|
14
14
|
static async getDevices() {
|
|
15
15
|
const result = await UsbStoragePlugin.getDevices();
|
|
16
16
|
return result.devices;
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
-
* USB
|
|
20
|
-
* @param filter
|
|
21
|
-
* @returns
|
|
19
|
+
* Request USB device access permission
|
|
20
|
+
* @param filter vendorId and productId of the USB device to request permission for
|
|
21
|
+
* @returns Whether permission was granted
|
|
22
22
|
*/
|
|
23
23
|
static async requestPermission(filter) {
|
|
24
24
|
const result = await UsbStoragePlugin.requestPermission(filter);
|
|
25
25
|
return result.granted;
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
|
-
* USB
|
|
29
|
-
* @param filter
|
|
30
|
-
* @returns
|
|
28
|
+
* Check if USB device access permission is granted
|
|
29
|
+
* @param filter vendorId and productId of the USB device to check permission for
|
|
30
|
+
* @returns Whether permission is held
|
|
31
31
|
*/
|
|
32
32
|
static async hasPermission(filter) {
|
|
33
33
|
const result = await UsbStoragePlugin.hasPermission(filter);
|
|
34
34
|
return result.granted;
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
38
|
-
* @param filter
|
|
39
|
-
* @param dirPath
|
|
40
|
-
* @returns
|
|
37
|
+
* Read directory contents from USB storage device
|
|
38
|
+
* @param filter vendorId and productId of the target USB device
|
|
39
|
+
* @param dirPath Directory path to read
|
|
40
|
+
* @returns Array of file/folder info in the directory
|
|
41
41
|
*/
|
|
42
42
|
static async readdir(filter, dirPath) {
|
|
43
43
|
const result = await UsbStoragePlugin.readdir({ ...filter, path: dirPath });
|
|
44
44
|
return result.files;
|
|
45
45
|
}
|
|
46
46
|
/**
|
|
47
|
-
* USB
|
|
48
|
-
* @param filter
|
|
49
|
-
* @param filePath
|
|
50
|
-
* @returns
|
|
47
|
+
* Read a file from USB storage device
|
|
48
|
+
* @param filter vendorId and productId of the target USB device
|
|
49
|
+
* @param filePath File path to read
|
|
50
|
+
* @returns Bytes containing file data, or undefined
|
|
51
51
|
*/
|
|
52
52
|
static async read(filter, filePath) {
|
|
53
53
|
const result = await UsbStoragePlugin.read({ ...filter, path: filePath });
|
|
@@ -22,7 +22,7 @@ export declare class UsbStorageWeb extends WebPlugin implements IUsbStoragePlugi
|
|
|
22
22
|
data: string | null;
|
|
23
23
|
}>;
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
25
|
+
* Add a virtual USB device. (For testing/development)
|
|
26
26
|
*/
|
|
27
27
|
addVirtualDevice(device: {
|
|
28
28
|
vendorId: number;
|
|
@@ -32,11 +32,11 @@ export declare class UsbStorageWeb extends WebPlugin implements IUsbStoragePlugi
|
|
|
32
32
|
productName: string;
|
|
33
33
|
}): Promise<void>;
|
|
34
34
|
/**
|
|
35
|
-
*
|
|
35
|
+
* Add a file to a virtual USB device. (For testing/development)
|
|
36
36
|
*/
|
|
37
37
|
addVirtualFile(filter: IUsbDeviceFilter, filePath: string, data: Uint8Array): Promise<void>;
|
|
38
38
|
/**
|
|
39
|
-
*
|
|
39
|
+
* Add a directory to a virtual USB device. (For testing/development)
|
|
40
40
|
*/
|
|
41
41
|
addVirtualDirectory(filter: IUsbDeviceFilter, dirPath: string): Promise<void>;
|
|
42
42
|
}
|
|
@@ -49,13 +49,13 @@ class UsbStorageWeb extends WebPlugin {
|
|
|
49
49
|
return { data: entry.dataBase64 };
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
52
|
-
*
|
|
52
|
+
* Add a virtual USB device. (For testing/development)
|
|
53
53
|
*/
|
|
54
54
|
async addVirtualDevice(device) {
|
|
55
55
|
await this._storage.addDevice(device);
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
58
|
-
*
|
|
58
|
+
* Add a file to a virtual USB device. (For testing/development)
|
|
59
59
|
*/
|
|
60
60
|
async addVirtualFile(filter, filePath, data) {
|
|
61
61
|
const deviceKey = `${filter.vendorId}:${filter.productId}`;
|
|
@@ -70,7 +70,7 @@ class UsbStorageWeb extends WebPlugin {
|
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
72
|
/**
|
|
73
|
-
*
|
|
73
|
+
* Add a directory to a virtual USB device. (For testing/development)
|
|
74
74
|
*/
|
|
75
75
|
async addVirtualDirectory(filter, dirPath) {
|
|
76
76
|
const deviceKey = `${filter.vendorId}:${filter.productId}`;
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simplysm/capacitor-plugin-usb-storage",
|
|
3
|
-
"version": "13.0.
|
|
4
|
-
"description": "
|
|
5
|
-
"author": "
|
|
3
|
+
"version": "13.0.71",
|
|
4
|
+
"description": "Simplysm Package - Capacitor USB Storage Plugin",
|
|
5
|
+
"author": "simplysm",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -14,10 +14,11 @@
|
|
|
14
14
|
"types": "./dist/index.d.ts",
|
|
15
15
|
"files": [
|
|
16
16
|
"dist",
|
|
17
|
+
"src",
|
|
17
18
|
"android"
|
|
18
19
|
],
|
|
19
20
|
"dependencies": {
|
|
20
|
-
"@simplysm/core-common": "13.0.
|
|
21
|
+
"@simplysm/core-common": "13.0.71"
|
|
21
22
|
},
|
|
22
23
|
"devDependencies": {
|
|
23
24
|
"@capacitor/core": "^7.5.0"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface IUsbDeviceInfo {
|
|
2
|
+
deviceName: string;
|
|
3
|
+
manufacturerName: string;
|
|
4
|
+
productName: string;
|
|
5
|
+
vendorId: number;
|
|
6
|
+
productId: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface IUsbDeviceFilter {
|
|
10
|
+
vendorId: number;
|
|
11
|
+
productId: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface IUsbFileInfo {
|
|
15
|
+
name: string;
|
|
16
|
+
isDirectory: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface IUsbStoragePlugin {
|
|
20
|
+
getDevices(): Promise<{ devices: IUsbDeviceInfo[] }>;
|
|
21
|
+
requestPermission(options: IUsbDeviceFilter): Promise<{ granted: boolean }>;
|
|
22
|
+
hasPermission(options: IUsbDeviceFilter): Promise<{ granted: boolean }>;
|
|
23
|
+
readdir(options: IUsbDeviceFilter & { path: string }): Promise<{ files: IUsbFileInfo[] }>;
|
|
24
|
+
read(options: IUsbDeviceFilter & { path: string }): Promise<{ data: string | null }>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { registerPlugin } from "@capacitor/core";
|
|
2
|
+
import type {
|
|
3
|
+
IUsbDeviceFilter,
|
|
4
|
+
IUsbDeviceInfo,
|
|
5
|
+
IUsbFileInfo,
|
|
6
|
+
IUsbStoragePlugin,
|
|
7
|
+
} from "./IUsbStoragePlugin";
|
|
8
|
+
import type { Bytes } from "@simplysm/core-common";
|
|
9
|
+
import { bytesFromBase64 } from "@simplysm/core-common";
|
|
10
|
+
|
|
11
|
+
const UsbStoragePlugin = registerPlugin<IUsbStoragePlugin>("UsbStorage", {
|
|
12
|
+
web: async () => {
|
|
13
|
+
const { UsbStorageWeb } = await import("./web/UsbStorageWeb");
|
|
14
|
+
return new UsbStorageWeb();
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Plugin for interacting with USB storage devices
|
|
20
|
+
* - Android: USB Mass Storage access via libaums library
|
|
21
|
+
* - Browser: IndexedDB-based virtual USB storage emulation
|
|
22
|
+
*/
|
|
23
|
+
export abstract class UsbStorage {
|
|
24
|
+
/**
|
|
25
|
+
* Get list of connected USB devices
|
|
26
|
+
* @returns Array of connected USB device info
|
|
27
|
+
*/
|
|
28
|
+
static async getDevices(): Promise<IUsbDeviceInfo[]> {
|
|
29
|
+
const result = await UsbStoragePlugin.getDevices();
|
|
30
|
+
return result.devices;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Request USB device access permission
|
|
35
|
+
* @param filter vendorId and productId of the USB device to request permission for
|
|
36
|
+
* @returns Whether permission was granted
|
|
37
|
+
*/
|
|
38
|
+
static async requestPermission(filter: IUsbDeviceFilter): Promise<boolean> {
|
|
39
|
+
const result = await UsbStoragePlugin.requestPermission(filter);
|
|
40
|
+
return result.granted;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if USB device access permission is granted
|
|
45
|
+
* @param filter vendorId and productId of the USB device to check permission for
|
|
46
|
+
* @returns Whether permission is held
|
|
47
|
+
*/
|
|
48
|
+
static async hasPermission(filter: IUsbDeviceFilter): Promise<boolean> {
|
|
49
|
+
const result = await UsbStoragePlugin.hasPermission(filter);
|
|
50
|
+
return result.granted;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Read directory contents from USB storage device
|
|
55
|
+
* @param filter vendorId and productId of the target USB device
|
|
56
|
+
* @param dirPath Directory path to read
|
|
57
|
+
* @returns Array of file/folder info in the directory
|
|
58
|
+
*/
|
|
59
|
+
static async readdir(filter: IUsbDeviceFilter, dirPath: string): Promise<IUsbFileInfo[]> {
|
|
60
|
+
const result = await UsbStoragePlugin.readdir({ ...filter, path: dirPath });
|
|
61
|
+
return result.files;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Read a file from USB storage device
|
|
66
|
+
* @param filter vendorId and productId of the target USB device
|
|
67
|
+
* @param filePath File path to read
|
|
68
|
+
* @returns Bytes containing file data, or undefined
|
|
69
|
+
*/
|
|
70
|
+
static async read(filter: IUsbDeviceFilter, filePath: string): Promise<Bytes | undefined> {
|
|
71
|
+
const result = await UsbStoragePlugin.read({ ...filter, path: filePath });
|
|
72
|
+
if (result.data == null) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
return bytesFromBase64(result.data);
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export interface IStoreConfig {
|
|
2
|
+
name: string;
|
|
3
|
+
keyPath: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export class IndexedDbStore {
|
|
7
|
+
constructor(
|
|
8
|
+
private readonly _dbName: string,
|
|
9
|
+
private readonly _dbVersion: number,
|
|
10
|
+
private readonly _storeConfigs: IStoreConfig[],
|
|
11
|
+
) {}
|
|
12
|
+
|
|
13
|
+
async open(): Promise<IDBDatabase> {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const req = indexedDB.open(this._dbName, this._dbVersion);
|
|
16
|
+
req.onupgradeneeded = () => {
|
|
17
|
+
const db = req.result;
|
|
18
|
+
for (const config of this._storeConfigs) {
|
|
19
|
+
if (!db.objectStoreNames.contains(config.name)) {
|
|
20
|
+
db.createObjectStore(config.name, { keyPath: config.keyPath });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
req.onsuccess = () => resolve(req.result);
|
|
25
|
+
req.onerror = () => reject(req.error);
|
|
26
|
+
req.onblocked = () => reject(new Error("Database blocked by another connection"));
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async withStore<T>(
|
|
31
|
+
storeName: string,
|
|
32
|
+
mode: IDBTransactionMode,
|
|
33
|
+
fn: (store: IDBObjectStore) => Promise<T>,
|
|
34
|
+
): Promise<T> {
|
|
35
|
+
const db = await this.open();
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const tx = db.transaction(storeName, mode);
|
|
38
|
+
const store = tx.objectStore(storeName);
|
|
39
|
+
let result: T;
|
|
40
|
+
Promise.resolve(fn(store))
|
|
41
|
+
.then((r) => {
|
|
42
|
+
result = r;
|
|
43
|
+
})
|
|
44
|
+
.catch((err) => {
|
|
45
|
+
db.close();
|
|
46
|
+
reject(err);
|
|
47
|
+
});
|
|
48
|
+
tx.oncomplete = () => {
|
|
49
|
+
db.close();
|
|
50
|
+
resolve(result);
|
|
51
|
+
};
|
|
52
|
+
tx.onerror = () => {
|
|
53
|
+
db.close();
|
|
54
|
+
reject(tx.error);
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async get<T>(storeName: string, key: IDBValidKey): Promise<T | undefined> {
|
|
60
|
+
return this.withStore(storeName, "readonly", async (store) => {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const req = store.get(key);
|
|
63
|
+
req.onsuccess = () => resolve(req.result as T | undefined);
|
|
64
|
+
req.onerror = () => reject(req.error);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async put(storeName: string, value: unknown): Promise<void> {
|
|
70
|
+
return this.withStore(storeName, "readwrite", async (store) => {
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const req = store.put(value);
|
|
73
|
+
req.onsuccess = () => resolve();
|
|
74
|
+
req.onerror = () => reject(req.error);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async getAll<T>(storeName: string): Promise<T[]> {
|
|
80
|
+
return this.withStore(storeName, "readonly", async (store) => {
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
const req = store.getAll();
|
|
83
|
+
req.onsuccess = () => resolve(req.result as T[]);
|
|
84
|
+
req.onerror = () => reject(req.error);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { WebPlugin } from "@capacitor/core";
|
|
2
|
+
import type {
|
|
3
|
+
IUsbDeviceFilter,
|
|
4
|
+
IUsbDeviceInfo,
|
|
5
|
+
IUsbFileInfo,
|
|
6
|
+
IUsbStoragePlugin,
|
|
7
|
+
} from "../IUsbStoragePlugin";
|
|
8
|
+
import { VirtualUsbStorage } from "./VirtualUsbStorage";
|
|
9
|
+
import { bytesToBase64 } from "@simplysm/core-common";
|
|
10
|
+
|
|
11
|
+
export class UsbStorageWeb extends WebPlugin implements IUsbStoragePlugin {
|
|
12
|
+
private readonly _storage = new VirtualUsbStorage();
|
|
13
|
+
|
|
14
|
+
async getDevices(): Promise<{ devices: IUsbDeviceInfo[] }> {
|
|
15
|
+
const devices = await this._storage.getDevices();
|
|
16
|
+
return {
|
|
17
|
+
devices: devices.map((d) => ({
|
|
18
|
+
deviceName: d.deviceName,
|
|
19
|
+
manufacturerName: d.manufacturerName,
|
|
20
|
+
productName: d.productName,
|
|
21
|
+
vendorId: d.vendorId,
|
|
22
|
+
productId: d.productId,
|
|
23
|
+
})),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async requestPermission(_options: IUsbDeviceFilter): Promise<{ granted: boolean }> {
|
|
28
|
+
return Promise.resolve({ granted: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async hasPermission(_options: IUsbDeviceFilter): Promise<{ granted: boolean }> {
|
|
32
|
+
return Promise.resolve({ granted: true });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async readdir(options: IUsbDeviceFilter & { path: string }): Promise<{ files: IUsbFileInfo[] }> {
|
|
36
|
+
const deviceKey = `${options.vendorId}:${options.productId}`;
|
|
37
|
+
const devices = await this._storage.getDevices();
|
|
38
|
+
const deviceExists = devices.some((d) => d.key === deviceKey);
|
|
39
|
+
if (!deviceExists) {
|
|
40
|
+
return { files: [] };
|
|
41
|
+
}
|
|
42
|
+
const entry = await this._storage.getEntry(deviceKey, options.path);
|
|
43
|
+
if (!entry || entry.kind !== "dir") {
|
|
44
|
+
return { files: [] };
|
|
45
|
+
}
|
|
46
|
+
const children = await this._storage.listChildren(deviceKey, options.path);
|
|
47
|
+
return { files: children };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async read(options: IUsbDeviceFilter & { path: string }): Promise<{ data: string | null }> {
|
|
51
|
+
const deviceKey = `${options.vendorId}:${options.productId}`;
|
|
52
|
+
const devices = await this._storage.getDevices();
|
|
53
|
+
const deviceExists = devices.some((d) => d.key === deviceKey);
|
|
54
|
+
if (!deviceExists) {
|
|
55
|
+
return { data: null };
|
|
56
|
+
}
|
|
57
|
+
const entry = await this._storage.getEntry(deviceKey, options.path);
|
|
58
|
+
if (!entry || entry.kind !== "file" || entry.dataBase64 == null) {
|
|
59
|
+
return { data: null };
|
|
60
|
+
}
|
|
61
|
+
return { data: entry.dataBase64 };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Add a virtual USB device. (For testing/development)
|
|
66
|
+
*/
|
|
67
|
+
async addVirtualDevice(device: {
|
|
68
|
+
vendorId: number;
|
|
69
|
+
productId: number;
|
|
70
|
+
deviceName: string;
|
|
71
|
+
manufacturerName: string;
|
|
72
|
+
productName: string;
|
|
73
|
+
}): Promise<void> {
|
|
74
|
+
await this._storage.addDevice(device);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Add a file to a virtual USB device. (For testing/development)
|
|
79
|
+
*/
|
|
80
|
+
async addVirtualFile(
|
|
81
|
+
filter: IUsbDeviceFilter,
|
|
82
|
+
filePath: string,
|
|
83
|
+
data: Uint8Array,
|
|
84
|
+
): Promise<void> {
|
|
85
|
+
const deviceKey = `${filter.vendorId}:${filter.productId}`;
|
|
86
|
+
const idx = filePath.lastIndexOf("/");
|
|
87
|
+
const dir = idx === -1 ? "/" : filePath.substring(0, idx) || "/";
|
|
88
|
+
await this._storage.ensureDir(deviceKey, dir);
|
|
89
|
+
await this._storage.putEntry({
|
|
90
|
+
deviceKey,
|
|
91
|
+
path: filePath,
|
|
92
|
+
kind: "file",
|
|
93
|
+
dataBase64: bytesToBase64(data),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Add a directory to a virtual USB device. (For testing/development)
|
|
99
|
+
*/
|
|
100
|
+
async addVirtualDirectory(filter: IUsbDeviceFilter, dirPath: string): Promise<void> {
|
|
101
|
+
const deviceKey = `${filter.vendorId}:${filter.productId}`;
|
|
102
|
+
await this._storage.ensureDir(deviceKey, dirPath);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { IndexedDbStore } from "./IndexedDbStore";
|
|
2
|
+
|
|
3
|
+
interface VirtualDevice {
|
|
4
|
+
key: string;
|
|
5
|
+
vendorId: number;
|
|
6
|
+
productId: number;
|
|
7
|
+
deviceName: string;
|
|
8
|
+
manufacturerName: string;
|
|
9
|
+
productName: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface VirtualEntry {
|
|
13
|
+
fullKey: string;
|
|
14
|
+
deviceKey: string;
|
|
15
|
+
path: string;
|
|
16
|
+
kind: "file" | "dir";
|
|
17
|
+
dataBase64?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class VirtualUsbStorage {
|
|
21
|
+
private readonly _DEVICES_STORE = "devices";
|
|
22
|
+
private readonly _FILES_STORE = "files";
|
|
23
|
+
private readonly _db: IndexedDbStore;
|
|
24
|
+
|
|
25
|
+
constructor() {
|
|
26
|
+
this._db = new IndexedDbStore("capacitor_usb_virtual_storage", 1, [
|
|
27
|
+
{ name: this._DEVICES_STORE, keyPath: "key" },
|
|
28
|
+
{ name: this._FILES_STORE, keyPath: "fullKey" },
|
|
29
|
+
]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async addDevice(device: Omit<VirtualDevice, "key">): Promise<void> {
|
|
33
|
+
const key = `${device.vendorId}:${device.productId}`;
|
|
34
|
+
const entry: VirtualDevice = { ...device, key };
|
|
35
|
+
return this._db.put(this._DEVICES_STORE, entry);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async getDevices(): Promise<VirtualDevice[]> {
|
|
39
|
+
return this._db.getAll<VirtualDevice>(this._DEVICES_STORE);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async getEntry(deviceKey: string, path: string): Promise<VirtualEntry | undefined> {
|
|
43
|
+
const fullKey = `${deviceKey}:${path}`;
|
|
44
|
+
return this._db.get<VirtualEntry>(this._FILES_STORE, fullKey);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async putEntry(entry: Omit<VirtualEntry, "fullKey">): Promise<void> {
|
|
48
|
+
const fullKey = `${entry.deviceKey}:${entry.path}`;
|
|
49
|
+
const fullEntry: VirtualEntry = { ...entry, fullKey };
|
|
50
|
+
return this._db.put(this._FILES_STORE, fullEntry);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async listChildren(
|
|
54
|
+
deviceKey: string,
|
|
55
|
+
dirPath: string,
|
|
56
|
+
): Promise<{ name: string; isDirectory: boolean }[]> {
|
|
57
|
+
const prefix = `${deviceKey}:${dirPath === "/" ? "/" : dirPath + "/"}`;
|
|
58
|
+
return this._db.withStore(this._FILES_STORE, "readonly", async (store) => {
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const req = store.openCursor();
|
|
61
|
+
const map = new Map<string, boolean>();
|
|
62
|
+
req.onsuccess = () => {
|
|
63
|
+
const cursor = req.result;
|
|
64
|
+
if (!cursor) {
|
|
65
|
+
resolve(
|
|
66
|
+
Array.from(map.entries()).map(([name, isDirectory]) => ({ name, isDirectory })),
|
|
67
|
+
);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const key = String(cursor.key);
|
|
71
|
+
if (key.startsWith(prefix)) {
|
|
72
|
+
const rest = key.slice(prefix.length);
|
|
73
|
+
if (rest) {
|
|
74
|
+
const segments = rest.split("/").filter(Boolean);
|
|
75
|
+
if (segments.length > 0) {
|
|
76
|
+
const firstName = segments[0];
|
|
77
|
+
if (!map.has(firstName)) {
|
|
78
|
+
const isDir =
|
|
79
|
+
segments.length > 1 || (cursor.value as VirtualEntry).kind === "dir";
|
|
80
|
+
map.set(firstName, isDir);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
cursor.continue();
|
|
86
|
+
};
|
|
87
|
+
req.onerror = () => reject(req.error);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async ensureDir(deviceKey: string, dirPath: string): Promise<void> {
|
|
93
|
+
if (dirPath === "/") {
|
|
94
|
+
await this.putEntry({ deviceKey, path: "/", kind: "dir" });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const segments = dirPath.split("/").filter(Boolean);
|
|
98
|
+
let acc = "";
|
|
99
|
+
for (const seg of segments) {
|
|
100
|
+
acc += "/" + seg;
|
|
101
|
+
const existing = await this.getEntry(deviceKey, acc);
|
|
102
|
+
if (!existing) {
|
|
103
|
+
await this.putEntry({ deviceKey, path: acc, kind: "dir" });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|