@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 CHANGED
@@ -1,267 +1,22 @@
1
1
  # @simplysm/capacitor-plugin-usb-storage
2
2
 
3
- A Capacitor plugin for accessing USB Mass Storage devices. On Android, it directly accesses the USB storage device's file system through the [libaums](https://github.com/magnusja/libaums) library, while in web environments it provides an IndexedDB-based virtual USB storage to support development and testing.
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
- ## Supported Platforms
9
+ **Peer Dependencies:** `@capacitor/core ^7.4.4`
13
10
 
14
- | Platform | Supported | Implementation |
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
- ### Android Requirements
13
+ ### USB Storage
21
14
 
22
- - `compileSdk`: 35
23
- - `minSdk`: 23 (Android 6.0 or higher)
24
- - Maximum file read size: 100MB
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
- MIT
22
+ Apache-2.0
@@ -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
- * USB 저장장치와 상호작용하기 위한 플러그인
5
- * - Android: libaums 라이브러리를 통한 USB Mass Storage 접근
6
- * - Browser: IndexedDB 기반 가상 USB 저장소 에뮬레이션
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
- * 연결된 USB 장치 목록을 가져옴
11
- * @returns 연결된 USB 장치 정보 배열
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 권한을 요청할 USB 장치의 vendorId와 productId
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 권한을 확인할 USB 장치의 vendorId와 productId
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
- * USB 저장장치의 디렉토리 내용을 읽어옴
28
- * @param filter 대상 USB 장치의 vendorId와 productId
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 대상 USB 장치의 vendorId와 productId
36
- * @param filePath 읽어올 파일 경로
37
- * @returns 파일 데이터를 담은 Bytes 또는 undefined
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
  }
@@ -8,46 +8,46 @@ const UsbStoragePlugin = registerPlugin("UsbStorage", {
8
8
  });
9
9
  class UsbStorage {
10
10
  /**
11
- * 연결된 USB 장치 목록을 가져옴
12
- * @returns 연결된 USB 장치 정보 배열
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 권한을 요청할 USB 장치의 vendorId와 productId
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 권한을 확인할 USB 장치의 vendorId와 productId
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
- * USB 저장장치의 디렉토리 내용을 읽어옴
38
- * @param filter 대상 USB 장치의 vendorId와 productId
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 대상 USB 장치의 vendorId와 productId
49
- * @param filePath 읽어올 파일 경로
50
- * @returns 파일 데이터를 담은 Bytes 또는 undefined
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
- * 가상 USB 장치를 추가합니다. (테스트/개발용)
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
- * 가상 USB 장치에 파일을 추가합니다. (테스트/개발용)
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
- * 가상 USB 장치에 디렉토리를 추가합니다. (테스트/개발용)
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
- * 가상 USB 장치를 추가합니다. (테스트/개발용)
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
- * 가상 USB 장치에 파일을 추가합니다. (테스트/개발용)
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
- * 가상 USB 장치에 디렉토리를 추가합니다. (테스트/개발용)
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.69",
4
- "description": "심플리즘 패키지 - Capacitor USB Storage Plugin",
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.69"
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,3 @@
1
+ // USB Storage
2
+ export * from "./IUsbStoragePlugin";
3
+ export * from "./UsbStorage";
@@ -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
+ }