@simplysm/capacitor-plugin-file-system 13.0.69 → 13.0.70
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 +10 -229
- package/dist/FileSystem.d.ts +25 -25
- package/dist/FileSystem.js +21 -21
- package/dist/web/FileSystemWeb.d.ts +3 -3
- package/dist/web/FileSystemWeb.js +3 -3
- package/dist/web/VirtualFileSystem.d.ts +6 -6
- package/dist/web/VirtualFileSystem.js +6 -6
- package/package.json +5 -4
- package/src/FileSystem.ts +126 -0
- package/src/IFileSystemPlugin.ts +26 -0
- package/src/index.ts +3 -0
- package/src/web/FileSystemWeb.ts +118 -0
- package/src/web/IndexedDbStore.ts +88 -0
- package/src/web/VirtualFileSystem.ts +107 -0
package/README.md
CHANGED
|
@@ -1,241 +1,22 @@
|
|
|
1
1
|
# @simplysm/capacitor-plugin-file-system
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Simplysm Package - Capacitor File System Plugin
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
npm install @simplysm/capacitor-plugin-file-system
|
|
9
|
-
npx cap sync
|
|
10
|
-
```
|
|
7
|
+
pnpm add @simplysm/capacitor-plugin-file-system
|
|
11
8
|
|
|
12
|
-
|
|
9
|
+
**Peer Dependencies:** `@capacitor/core ^7.4.4`
|
|
13
10
|
|
|
14
|
-
|
|
15
|
-
|--------|----------|----------|
|
|
16
|
-
| Android | Yes | Native Java (API 23+) |
|
|
17
|
-
| Web | Yes | IndexedDB-based virtual file system |
|
|
18
|
-
| iOS | No | - |
|
|
11
|
+
## Source Index
|
|
19
12
|
|
|
20
|
-
###
|
|
13
|
+
### File System
|
|
21
14
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
|
25
|
-
|
|
26
|
-
| Android 11+ (API 30+) | `MANAGE_EXTERNAL_STORAGE` | Navigate to settings screen to grant full file access permission |
|
|
27
|
-
| Android 10 and below | `READ_EXTERNAL_STORAGE`, `WRITE_EXTERNAL_STORAGE` | Display runtime permission dialog |
|
|
28
|
-
|
|
29
|
-
The plugin automatically declares the necessary permissions in `AndroidManifest.xml`, so you don't need to add them separately in your app's manifest.
|
|
30
|
-
|
|
31
|
-
## Main Modules
|
|
32
|
-
|
|
33
|
-
All APIs are provided as static methods of the `FileSystem` class.
|
|
34
|
-
|
|
35
|
-
```typescript
|
|
36
|
-
import { FileSystem } from "@simplysm/capacitor-plugin-file-system";
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
### Method List
|
|
40
|
-
|
|
41
|
-
| Method | Return Type | Description |
|
|
42
|
-
|--------|----------|------|
|
|
43
|
-
| `hasPermission()` | `Promise<boolean>` | Check if file system access permission is granted |
|
|
44
|
-
| `requestPermission()` | `Promise<void>` | Request file system access permission |
|
|
45
|
-
| `readdir(dirPath)` | `Promise<IFileInfo[]>` | List files/folders in directory |
|
|
46
|
-
| `getStoragePath(type)` | `Promise<string>` | Return absolute path by storage type |
|
|
47
|
-
| `getFileUri(filePath)` | `Promise<string>` | Return FileProvider URI (Android) / Blob URL (Web) |
|
|
48
|
-
| `writeFile(filePath, data)` | `Promise<void>` | Write file (string or binary) |
|
|
49
|
-
| `readFileString(filePath)` | `Promise<string>` | Read file as UTF-8 string |
|
|
50
|
-
| `readFileBytes(filePath)` | `Promise<Bytes>` | Read file as binary (`Uint8Array`) |
|
|
51
|
-
| `remove(targetPath)` | `Promise<void>` | Recursively delete file or directory |
|
|
52
|
-
| `mkdir(targetPath)` | `Promise<void>` | Recursively create directory |
|
|
53
|
-
| `exists(targetPath)` | `Promise<boolean>` | Check if file or directory exists |
|
|
54
|
-
|
|
55
|
-
### Type Definitions
|
|
56
|
-
|
|
57
|
-
```typescript
|
|
58
|
-
import type { TStorage, IFileInfo } from "@simplysm/capacitor-plugin-file-system";
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
#### `TStorage`
|
|
62
|
-
|
|
63
|
-
A string literal type representing storage types.
|
|
64
|
-
|
|
65
|
-
| Value | Android Path | Description |
|
|
66
|
-
|----|-------------|------|
|
|
67
|
-
| `"external"` | `Environment.getExternalStorageDirectory()` | External storage root |
|
|
68
|
-
| `"externalFiles"` | `Context.getExternalFilesDir(null)` | App-specific external files directory |
|
|
69
|
-
| `"externalCache"` | `Context.getExternalCacheDir()` | App-specific external cache directory |
|
|
70
|
-
| `"externalMedia"` | `Context.getExternalMediaDirs()[0]` | App-specific external media directory |
|
|
71
|
-
| `"appData"` | `ApplicationInfo.dataDir` | App data directory |
|
|
72
|
-
| `"appFiles"` | `Context.getFilesDir()` | App internal files directory |
|
|
73
|
-
| `"appCache"` | `Context.getCacheDir()` | App internal cache directory |
|
|
74
|
-
|
|
75
|
-
#### `IFileInfo`
|
|
76
|
-
|
|
77
|
-
```typescript
|
|
78
|
-
interface IFileInfo {
|
|
79
|
-
name: string; // File or directory name
|
|
80
|
-
isDirectory: boolean; // Whether it's a directory
|
|
81
|
-
}
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
#### `IFileSystemPlugin`
|
|
85
|
-
|
|
86
|
-
The low-level Capacitor plugin interface. Not intended for direct use -- use the `FileSystem` static class instead.
|
|
87
|
-
|
|
88
|
-
```typescript
|
|
89
|
-
import type { IFileSystemPlugin } from "@simplysm/capacitor-plugin-file-system";
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
## Usage Examples
|
|
93
|
-
|
|
94
|
-
### Check and Request Permission
|
|
95
|
-
|
|
96
|
-
```typescript
|
|
97
|
-
import { FileSystem } from "@simplysm/capacitor-plugin-file-system";
|
|
98
|
-
|
|
99
|
-
async function ensurePermission(): Promise<boolean> {
|
|
100
|
-
const granted = await FileSystem.hasPermission();
|
|
101
|
-
if (!granted) {
|
|
102
|
-
await FileSystem.requestPermission();
|
|
103
|
-
// On Android 11+, it navigates to settings screen,
|
|
104
|
-
// so you need to check again after returning to the app.
|
|
105
|
-
return await FileSystem.hasPermission();
|
|
106
|
-
}
|
|
107
|
-
return true;
|
|
108
|
-
}
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
### Read/Write Text Files
|
|
112
|
-
|
|
113
|
-
```typescript
|
|
114
|
-
import { FileSystem } from "@simplysm/capacitor-plugin-file-system";
|
|
115
|
-
|
|
116
|
-
async function textFileExample(): Promise<void> {
|
|
117
|
-
const storagePath = await FileSystem.getStoragePath("appFiles");
|
|
118
|
-
const filePath = storagePath + "/config.json";
|
|
119
|
-
|
|
120
|
-
// Write file
|
|
121
|
-
const config = { theme: "dark", lang: "ko" };
|
|
122
|
-
await FileSystem.writeFile(filePath, JSON.stringify(config, null, 2));
|
|
123
|
-
|
|
124
|
-
// Read file
|
|
125
|
-
const content = await FileSystem.readFileString(filePath);
|
|
126
|
-
const parsed = JSON.parse(content);
|
|
127
|
-
console.log(parsed.theme); // "dark"
|
|
128
|
-
}
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
### Read/Write Binary Files
|
|
132
|
-
|
|
133
|
-
```typescript
|
|
134
|
-
import { FileSystem } from "@simplysm/capacitor-plugin-file-system";
|
|
135
|
-
import type { Bytes } from "@simplysm/core-common";
|
|
136
|
-
|
|
137
|
-
async function binaryFileExample(): Promise<void> {
|
|
138
|
-
const storagePath = await FileSystem.getStoragePath("appFiles");
|
|
139
|
-
const filePath = storagePath + "/data.bin";
|
|
140
|
-
|
|
141
|
-
// Write as Uint8Array
|
|
142
|
-
const bytes = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]);
|
|
143
|
-
await FileSystem.writeFile(filePath, bytes);
|
|
144
|
-
|
|
145
|
-
// Read as Bytes (Uint8Array alias from @simplysm/core-common)
|
|
146
|
-
const readBytes: Bytes = await FileSystem.readFileBytes(filePath);
|
|
147
|
-
console.log(readBytes.length); // 5
|
|
148
|
-
}
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
### Directory Management
|
|
152
|
-
|
|
153
|
-
```typescript
|
|
154
|
-
import { FileSystem } from "@simplysm/capacitor-plugin-file-system";
|
|
155
|
-
|
|
156
|
-
async function directoryExample(): Promise<void> {
|
|
157
|
-
const storagePath = await FileSystem.getStoragePath("appFiles");
|
|
158
|
-
const dirPath = storagePath + "/logs/2024";
|
|
159
|
-
|
|
160
|
-
// Recursively create directory
|
|
161
|
-
await FileSystem.mkdir(dirPath);
|
|
162
|
-
|
|
163
|
-
// Create file
|
|
164
|
-
await FileSystem.writeFile(dirPath + "/app.log", "Started\n");
|
|
165
|
-
|
|
166
|
-
// List directory contents
|
|
167
|
-
const files = await FileSystem.readdir(dirPath);
|
|
168
|
-
for (const file of files) {
|
|
169
|
-
console.log(`${file.name} (directory: ${file.isDirectory})`);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Check existence
|
|
173
|
-
const dirExists = await FileSystem.exists(dirPath);
|
|
174
|
-
console.log(dirExists); // true
|
|
175
|
-
|
|
176
|
-
// Recursively delete directory
|
|
177
|
-
await FileSystem.remove(dirPath);
|
|
178
|
-
}
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
### Get FileProvider URI
|
|
182
|
-
|
|
183
|
-
On Android, a `content://` URI is needed when sharing files with other apps.
|
|
184
|
-
|
|
185
|
-
```typescript
|
|
186
|
-
import { FileSystem } from "@simplysm/capacitor-plugin-file-system";
|
|
187
|
-
|
|
188
|
-
async function shareFile(filePath: string): Promise<string> {
|
|
189
|
-
// Android: returns content:// URI
|
|
190
|
-
// Web: returns blob: URL (must call URL.revokeObjectURL() after use)
|
|
191
|
-
const uri = await FileSystem.getFileUri(filePath);
|
|
192
|
-
return uri;
|
|
193
|
-
}
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
> **Warning**: In web environments, the Blob URL returned by `getFileUri()` must be released by calling `URL.revokeObjectURL(uri)` after use to free memory.
|
|
197
|
-
|
|
198
|
-
## Android Configuration
|
|
199
|
-
|
|
200
|
-
### FileProvider
|
|
201
|
-
|
|
202
|
-
The plugin includes its own `FileSystemProvider`, and the authority is automatically set in the format `${applicationId}.filesystem.provider`. Shareable paths are:
|
|
203
|
-
|
|
204
|
-
- External storage (`external-path`)
|
|
205
|
-
- App-specific external files (`external-files-path`)
|
|
206
|
-
- App-specific external cache (`external-cache-path`)
|
|
207
|
-
- App internal files (`files-path`)
|
|
208
|
-
- App internal cache (`cache-path`)
|
|
209
|
-
|
|
210
|
-
### Minimum SDK Version
|
|
211
|
-
|
|
212
|
-
- `minSdkVersion`: 23 (Android 6.0)
|
|
213
|
-
- `compileSdkVersion`: 35
|
|
214
|
-
|
|
215
|
-
## Web Environment Behavior
|
|
216
|
-
|
|
217
|
-
In web environments, it operates as an IndexedDB-based virtual file system (`VirtualFileSystem`).
|
|
218
|
-
|
|
219
|
-
- Database name: `capacitor_web_virtual_fs`
|
|
220
|
-
- Permission-related methods (`hasPermission`, `requestPermission`) always succeed.
|
|
221
|
-
- `getStoragePath()` returns virtual paths in the format `/webfs/{type}`.
|
|
222
|
-
- `getFileUri()` returns a Blob URL, which must be released with `URL.revokeObjectURL()` after use.
|
|
223
|
-
- File data is base64-encoded and stored in IndexedDB.
|
|
224
|
-
|
|
225
|
-
## Dependencies
|
|
226
|
-
|
|
227
|
-
### Peer Dependencies
|
|
228
|
-
|
|
229
|
-
| Package | Version |
|
|
230
|
-
|--------|------|
|
|
231
|
-
| `@capacitor/core` | `^7.4.4` |
|
|
232
|
-
|
|
233
|
-
### Internal Dependencies
|
|
234
|
-
|
|
235
|
-
| Package | Description |
|
|
236
|
-
|--------|------|
|
|
237
|
-
| `@simplysm/core-common` | Common utilities such as base64 conversion, `Bytes` type |
|
|
15
|
+
| Source | Exports | Description | Test |
|
|
16
|
+
|--------|---------|-------------|------|
|
|
17
|
+
| `src/FileSystem.ts` | `FileSystem` | Static class for file read/write, directory, permission, and URI operations | - |
|
|
18
|
+
| `src/IFileSystemPlugin.ts` | `TStorage`, `IFileInfo`, `IFileSystemPlugin` | Types and interface for storage paths, file info, and the native plugin contract | - |
|
|
238
19
|
|
|
239
20
|
## License
|
|
240
21
|
|
|
241
|
-
|
|
22
|
+
Apache-2.0
|
package/dist/FileSystem.d.ts
CHANGED
|
@@ -1,64 +1,64 @@
|
|
|
1
1
|
import type { IFileInfo, TStorage } from "./IFileSystemPlugin";
|
|
2
2
|
import type { Bytes } from "@simplysm/core-common";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* - Android 11+:
|
|
6
|
-
* - Android 10-: READ/WRITE_EXTERNAL_STORAGE
|
|
7
|
-
* - Browser: IndexedDB
|
|
4
|
+
* File system access plugin
|
|
5
|
+
* - Android 11+: Full file system access via MANAGE_EXTERNAL_STORAGE permission
|
|
6
|
+
* - Android 10-: READ/WRITE_EXTERNAL_STORAGE permission
|
|
7
|
+
* - Browser: IndexedDB-based emulation
|
|
8
8
|
*/
|
|
9
9
|
export declare abstract class FileSystem {
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
11
|
+
* Check permission
|
|
12
12
|
*/
|
|
13
13
|
static hasPermission(): Promise<boolean>;
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
* - Android 11+:
|
|
17
|
-
* - Android 10-:
|
|
15
|
+
* Request permission
|
|
16
|
+
* - Android 11+: Navigate to settings
|
|
17
|
+
* - Android 10-: Show permission dialog
|
|
18
18
|
*/
|
|
19
19
|
static requestPermission(): Promise<void>;
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
21
|
+
* Read directory
|
|
22
22
|
*/
|
|
23
23
|
static readdir(dirPath: string): Promise<IFileInfo[]>;
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
26
|
-
* @param type
|
|
27
|
-
* - external:
|
|
28
|
-
* - externalFiles:
|
|
29
|
-
* - externalCache:
|
|
30
|
-
* - externalMedia:
|
|
31
|
-
* - appData:
|
|
32
|
-
* - appFiles:
|
|
33
|
-
* - appCache:
|
|
25
|
+
* Get storage path
|
|
26
|
+
* @param type Storage type
|
|
27
|
+
* - external: External storage root (Environment.getExternalStorageDirectory)
|
|
28
|
+
* - externalFiles: App-specific external files directory
|
|
29
|
+
* - externalCache: App-specific external cache directory
|
|
30
|
+
* - externalMedia: App-specific external media directory
|
|
31
|
+
* - appData: App data directory
|
|
32
|
+
* - appFiles: App files directory
|
|
33
|
+
* - appCache: App cache directory
|
|
34
34
|
*/
|
|
35
35
|
static getStoragePath(type: TStorage): Promise<string>;
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
37
|
+
* Get file URI (FileProvider)
|
|
38
38
|
*/
|
|
39
39
|
static getFileUri(filePath: string): Promise<string>;
|
|
40
40
|
/**
|
|
41
|
-
*
|
|
41
|
+
* Write file
|
|
42
42
|
*/
|
|
43
43
|
static writeFile(filePath: string, data: string | Bytes): Promise<void>;
|
|
44
44
|
/**
|
|
45
|
-
*
|
|
45
|
+
* Read file (UTF-8 string)
|
|
46
46
|
*/
|
|
47
47
|
static readFileString(filePath: string): Promise<string>;
|
|
48
48
|
/**
|
|
49
|
-
*
|
|
49
|
+
* Read file (Bytes)
|
|
50
50
|
*/
|
|
51
51
|
static readFileBytes(filePath: string): Promise<Bytes>;
|
|
52
52
|
/**
|
|
53
|
-
*
|
|
53
|
+
* Delete file/directory (recursive)
|
|
54
54
|
*/
|
|
55
55
|
static remove(targetPath: string): Promise<void>;
|
|
56
56
|
/**
|
|
57
|
-
*
|
|
57
|
+
* Create directory (recursive)
|
|
58
58
|
*/
|
|
59
59
|
static mkdir(targetPath: string): Promise<void>;
|
|
60
60
|
/**
|
|
61
|
-
*
|
|
61
|
+
* Check existence
|
|
62
62
|
*/
|
|
63
63
|
static exists(targetPath: string): Promise<boolean>;
|
|
64
64
|
}
|
package/dist/FileSystem.js
CHANGED
|
@@ -8,51 +8,51 @@ const FileSystemPlugin = registerPlugin("FileSystem", {
|
|
|
8
8
|
});
|
|
9
9
|
class FileSystem {
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
11
|
+
* Check permission
|
|
12
12
|
*/
|
|
13
13
|
static async hasPermission() {
|
|
14
14
|
const result = await FileSystemPlugin.hasPermission();
|
|
15
15
|
return result.granted;
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
* - Android 11+:
|
|
20
|
-
* - Android 10-:
|
|
18
|
+
* Request permission
|
|
19
|
+
* - Android 11+: Navigate to settings
|
|
20
|
+
* - Android 10-: Show permission dialog
|
|
21
21
|
*/
|
|
22
22
|
static async requestPermission() {
|
|
23
23
|
await FileSystemPlugin.requestPermission();
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
26
|
+
* Read directory
|
|
27
27
|
*/
|
|
28
28
|
static async readdir(dirPath) {
|
|
29
29
|
const result = await FileSystemPlugin.readdir({ path: dirPath });
|
|
30
30
|
return result.files;
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
|
-
*
|
|
34
|
-
* @param type
|
|
35
|
-
* - external:
|
|
36
|
-
* - externalFiles:
|
|
37
|
-
* - externalCache:
|
|
38
|
-
* - externalMedia:
|
|
39
|
-
* - appData:
|
|
40
|
-
* - appFiles:
|
|
41
|
-
* - appCache:
|
|
33
|
+
* Get storage path
|
|
34
|
+
* @param type Storage type
|
|
35
|
+
* - external: External storage root (Environment.getExternalStorageDirectory)
|
|
36
|
+
* - externalFiles: App-specific external files directory
|
|
37
|
+
* - externalCache: App-specific external cache directory
|
|
38
|
+
* - externalMedia: App-specific external media directory
|
|
39
|
+
* - appData: App data directory
|
|
40
|
+
* - appFiles: App files directory
|
|
41
|
+
* - appCache: App cache directory
|
|
42
42
|
*/
|
|
43
43
|
static async getStoragePath(type) {
|
|
44
44
|
const result = await FileSystemPlugin.getStoragePath({ type });
|
|
45
45
|
return result.path;
|
|
46
46
|
}
|
|
47
47
|
/**
|
|
48
|
-
*
|
|
48
|
+
* Get file URI (FileProvider)
|
|
49
49
|
*/
|
|
50
50
|
static async getFileUri(filePath) {
|
|
51
51
|
const result = await FileSystemPlugin.getFileUri({ path: filePath });
|
|
52
52
|
return result.uri;
|
|
53
53
|
}
|
|
54
54
|
/**
|
|
55
|
-
*
|
|
55
|
+
* Write file
|
|
56
56
|
*/
|
|
57
57
|
static async writeFile(filePath, data) {
|
|
58
58
|
if (typeof data !== "string") {
|
|
@@ -70,33 +70,33 @@ class FileSystem {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
/**
|
|
73
|
-
*
|
|
73
|
+
* Read file (UTF-8 string)
|
|
74
74
|
*/
|
|
75
75
|
static async readFileString(filePath) {
|
|
76
76
|
const result = await FileSystemPlugin.readFile({ path: filePath, encoding: "utf8" });
|
|
77
77
|
return result.data;
|
|
78
78
|
}
|
|
79
79
|
/**
|
|
80
|
-
*
|
|
80
|
+
* Read file (Bytes)
|
|
81
81
|
*/
|
|
82
82
|
static async readFileBytes(filePath) {
|
|
83
83
|
const result = await FileSystemPlugin.readFile({ path: filePath, encoding: "base64" });
|
|
84
84
|
return bytesFromBase64(result.data);
|
|
85
85
|
}
|
|
86
86
|
/**
|
|
87
|
-
*
|
|
87
|
+
* Delete file/directory (recursive)
|
|
88
88
|
*/
|
|
89
89
|
static async remove(targetPath) {
|
|
90
90
|
await FileSystemPlugin.remove({ path: targetPath });
|
|
91
91
|
}
|
|
92
92
|
/**
|
|
93
|
-
*
|
|
93
|
+
* Create directory (recursive)
|
|
94
94
|
*/
|
|
95
95
|
static async mkdir(targetPath) {
|
|
96
96
|
await FileSystemPlugin.mkdir({ path: targetPath });
|
|
97
97
|
}
|
|
98
98
|
/**
|
|
99
|
-
*
|
|
99
|
+
* Check existence
|
|
100
100
|
*/
|
|
101
101
|
static async exists(targetPath) {
|
|
102
102
|
const result = await FileSystemPlugin.exists({ path: targetPath });
|
|
@@ -19,9 +19,9 @@ export declare class FileSystemWeb extends WebPlugin implements IFileSystemPlugi
|
|
|
19
19
|
path: string;
|
|
20
20
|
}>;
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
23
|
-
* @warning
|
|
24
|
-
*
|
|
22
|
+
* Return the Blob URL of a file.
|
|
23
|
+
* @warning The returned URI must be released by calling `URL.revokeObjectURL(uri)` after use.
|
|
24
|
+
* Failure to release may cause memory leaks.
|
|
25
25
|
*/
|
|
26
26
|
getFileUri(options: {
|
|
27
27
|
path: string;
|
|
@@ -50,9 +50,9 @@ class FileSystemWeb extends WebPlugin {
|
|
|
50
50
|
return { path: storagePath };
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
|
-
*
|
|
54
|
-
* @warning
|
|
55
|
-
*
|
|
53
|
+
* Return the Blob URL of a file.
|
|
54
|
+
* @warning The returned URI must be released by calling `URL.revokeObjectURL(uri)` after use.
|
|
55
|
+
* Failure to release may cause memory leaks.
|
|
56
56
|
*/
|
|
57
57
|
async getFileUri(options) {
|
|
58
58
|
const entry = await this._fs.getEntry(options.path);
|
|
@@ -12,12 +12,12 @@ export declare class VirtualFileSystem {
|
|
|
12
12
|
putEntry(entry: FsEntry): Promise<void>;
|
|
13
13
|
deleteByPrefix(pathPrefix: string): Promise<boolean>;
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
* @param dirPath
|
|
17
|
-
* @returns
|
|
18
|
-
* @note
|
|
19
|
-
*
|
|
20
|
-
* listChildren("/a")
|
|
15
|
+
* Return the direct children of a directory.
|
|
16
|
+
* @param dirPath Directory path to query
|
|
17
|
+
* @returns List of child files/directories
|
|
18
|
+
* @note Implicit directory handling: Even when only file paths exist without directory entries,
|
|
19
|
+
* intermediate paths are treated as directories. e.g., With only "/a/b/c.txt" stored,
|
|
20
|
+
* calling listChildren("/a") returns "b" with isDirectory: true.
|
|
21
21
|
*/
|
|
22
22
|
listChildren(dirPath: string): Promise<IFileInfo[]>;
|
|
23
23
|
ensureDir(dirPath: string): Promise<void>;
|
|
@@ -34,12 +34,12 @@ class VirtualFileSystem {
|
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
38
|
-
* @param dirPath
|
|
39
|
-
* @returns
|
|
40
|
-
* @note
|
|
41
|
-
*
|
|
42
|
-
* listChildren("/a")
|
|
37
|
+
* Return the direct children of a directory.
|
|
38
|
+
* @param dirPath Directory path to query
|
|
39
|
+
* @returns List of child files/directories
|
|
40
|
+
* @note Implicit directory handling: Even when only file paths exist without directory entries,
|
|
41
|
+
* intermediate paths are treated as directories. e.g., With only "/a/b/c.txt" stored,
|
|
42
|
+
* calling listChildren("/a") returns "b" with isDirectory: true.
|
|
43
43
|
*/
|
|
44
44
|
async listChildren(dirPath) {
|
|
45
45
|
const prefix = dirPath === "/" ? "/" : dirPath + "/";
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simplysm/capacitor-plugin-file-system",
|
|
3
|
-
"version": "13.0.
|
|
4
|
-
"description": "
|
|
5
|
-
"author": "
|
|
3
|
+
"version": "13.0.70",
|
|
4
|
+
"description": "Simplysm Package - Capacitor File System 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.70"
|
|
21
22
|
},
|
|
22
23
|
"devDependencies": {
|
|
23
24
|
"@capacitor/core": "^7.5.0"
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { registerPlugin } from "@capacitor/core";
|
|
2
|
+
import type { IFileInfo, IFileSystemPlugin, TStorage } from "./IFileSystemPlugin";
|
|
3
|
+
import type { Bytes } from "@simplysm/core-common";
|
|
4
|
+
import { bytesToBase64, bytesFromBase64 } from "@simplysm/core-common";
|
|
5
|
+
|
|
6
|
+
const FileSystemPlugin = registerPlugin<IFileSystemPlugin>("FileSystem", {
|
|
7
|
+
web: async () => {
|
|
8
|
+
const { FileSystemWeb } = await import("./web/FileSystemWeb");
|
|
9
|
+
return new FileSystemWeb();
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* File system access plugin
|
|
15
|
+
* - Android 11+: Full file system access via MANAGE_EXTERNAL_STORAGE permission
|
|
16
|
+
* - Android 10-: READ/WRITE_EXTERNAL_STORAGE permission
|
|
17
|
+
* - Browser: IndexedDB-based emulation
|
|
18
|
+
*/
|
|
19
|
+
export abstract class FileSystem {
|
|
20
|
+
/**
|
|
21
|
+
* Check permission
|
|
22
|
+
*/
|
|
23
|
+
static async hasPermission(): Promise<boolean> {
|
|
24
|
+
const result = await FileSystemPlugin.hasPermission();
|
|
25
|
+
return result.granted;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Request permission
|
|
30
|
+
* - Android 11+: Navigate to settings
|
|
31
|
+
* - Android 10-: Show permission dialog
|
|
32
|
+
*/
|
|
33
|
+
static async requestPermission(): Promise<void> {
|
|
34
|
+
await FileSystemPlugin.requestPermission();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Read directory
|
|
39
|
+
*/
|
|
40
|
+
static async readdir(dirPath: string): Promise<IFileInfo[]> {
|
|
41
|
+
const result = await FileSystemPlugin.readdir({ path: dirPath });
|
|
42
|
+
return result.files;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get storage path
|
|
47
|
+
* @param type Storage type
|
|
48
|
+
* - external: External storage root (Environment.getExternalStorageDirectory)
|
|
49
|
+
* - externalFiles: App-specific external files directory
|
|
50
|
+
* - externalCache: App-specific external cache directory
|
|
51
|
+
* - externalMedia: App-specific external media directory
|
|
52
|
+
* - appData: App data directory
|
|
53
|
+
* - appFiles: App files directory
|
|
54
|
+
* - appCache: App cache directory
|
|
55
|
+
*/
|
|
56
|
+
static async getStoragePath(type: TStorage): Promise<string> {
|
|
57
|
+
const result = await FileSystemPlugin.getStoragePath({ type });
|
|
58
|
+
return result.path;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get file URI (FileProvider)
|
|
63
|
+
*/
|
|
64
|
+
static async getFileUri(filePath: string): Promise<string> {
|
|
65
|
+
const result = await FileSystemPlugin.getFileUri({ path: filePath });
|
|
66
|
+
return result.uri;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Write file
|
|
71
|
+
*/
|
|
72
|
+
static async writeFile(filePath: string, data: string | Bytes): Promise<void> {
|
|
73
|
+
if (typeof data !== "string") {
|
|
74
|
+
// Bytes (Uint8Array) - works safely in cross-realm environments
|
|
75
|
+
await FileSystemPlugin.writeFile({
|
|
76
|
+
path: filePath,
|
|
77
|
+
data: bytesToBase64(data),
|
|
78
|
+
encoding: "base64",
|
|
79
|
+
});
|
|
80
|
+
} else {
|
|
81
|
+
await FileSystemPlugin.writeFile({
|
|
82
|
+
path: filePath,
|
|
83
|
+
data,
|
|
84
|
+
encoding: "utf8",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Read file (UTF-8 string)
|
|
91
|
+
*/
|
|
92
|
+
static async readFileString(filePath: string): Promise<string> {
|
|
93
|
+
const result = await FileSystemPlugin.readFile({ path: filePath, encoding: "utf8" });
|
|
94
|
+
return result.data;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Read file (Bytes)
|
|
99
|
+
*/
|
|
100
|
+
static async readFileBytes(filePath: string): Promise<Bytes> {
|
|
101
|
+
const result = await FileSystemPlugin.readFile({ path: filePath, encoding: "base64" });
|
|
102
|
+
return bytesFromBase64(result.data);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Delete file/directory (recursive)
|
|
107
|
+
*/
|
|
108
|
+
static async remove(targetPath: string): Promise<void> {
|
|
109
|
+
await FileSystemPlugin.remove({ path: targetPath });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Create directory (recursive)
|
|
114
|
+
*/
|
|
115
|
+
static async mkdir(targetPath: string): Promise<void> {
|
|
116
|
+
await FileSystemPlugin.mkdir({ path: targetPath });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Check existence
|
|
121
|
+
*/
|
|
122
|
+
static async exists(targetPath: string): Promise<boolean> {
|
|
123
|
+
const result = await FileSystemPlugin.exists({ path: targetPath });
|
|
124
|
+
return result.exists;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type TStorage =
|
|
2
|
+
| "external"
|
|
3
|
+
| "externalFiles"
|
|
4
|
+
| "externalCache"
|
|
5
|
+
| "externalMedia"
|
|
6
|
+
| "appData"
|
|
7
|
+
| "appFiles"
|
|
8
|
+
| "appCache";
|
|
9
|
+
|
|
10
|
+
export interface IFileInfo {
|
|
11
|
+
name: string;
|
|
12
|
+
isDirectory: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface IFileSystemPlugin {
|
|
16
|
+
hasPermission(): Promise<{ granted: boolean }>;
|
|
17
|
+
requestPermission(): Promise<void>;
|
|
18
|
+
readdir(options: { path: string }): Promise<{ files: IFileInfo[] }>;
|
|
19
|
+
getStoragePath(options: { type: TStorage }): Promise<{ path: string }>;
|
|
20
|
+
getFileUri(options: { path: string }): Promise<{ uri: string }>;
|
|
21
|
+
writeFile(options: { path: string; data: string; encoding?: "utf8" | "base64" }): Promise<void>;
|
|
22
|
+
readFile(options: { path: string; encoding?: "utf8" | "base64" }): Promise<{ data: string }>;
|
|
23
|
+
remove(options: { path: string }): Promise<void>;
|
|
24
|
+
mkdir(options: { path: string }): Promise<void>;
|
|
25
|
+
exists(options: { path: string }): Promise<{ exists: boolean }>;
|
|
26
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { WebPlugin } from "@capacitor/core";
|
|
2
|
+
import type { IFileInfo, IFileSystemPlugin, TStorage } from "../IFileSystemPlugin";
|
|
3
|
+
import { VirtualFileSystem } from "./VirtualFileSystem";
|
|
4
|
+
import { bytesToBase64, bytesFromBase64 } from "@simplysm/core-common";
|
|
5
|
+
|
|
6
|
+
export class FileSystemWeb extends WebPlugin implements IFileSystemPlugin {
|
|
7
|
+
private readonly _fs = new VirtualFileSystem("capacitor_web_virtual_fs");
|
|
8
|
+
private readonly _textEncoder = new TextEncoder();
|
|
9
|
+
private readonly _textDecoder = new TextDecoder();
|
|
10
|
+
|
|
11
|
+
async hasPermission(): Promise<{ granted: boolean }> {
|
|
12
|
+
return Promise.resolve({ granted: true });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async requestPermission(): Promise<void> {}
|
|
16
|
+
|
|
17
|
+
async readdir(options: { path: string }): Promise<{ files: IFileInfo[] }> {
|
|
18
|
+
const entry = await this._fs.getEntry(options.path);
|
|
19
|
+
if (!entry || entry.kind !== "dir") {
|
|
20
|
+
throw new Error("Directory does not exist");
|
|
21
|
+
}
|
|
22
|
+
const files = await this._fs.listChildren(options.path);
|
|
23
|
+
return { files };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async getStoragePath(options: { type: TStorage }): Promise<{ path: string }> {
|
|
27
|
+
const base = "/webfs";
|
|
28
|
+
let storagePath: string;
|
|
29
|
+
switch (options.type) {
|
|
30
|
+
case "external":
|
|
31
|
+
storagePath = base + "/external";
|
|
32
|
+
break;
|
|
33
|
+
case "externalFiles":
|
|
34
|
+
storagePath = base + "/externalFiles";
|
|
35
|
+
break;
|
|
36
|
+
case "externalCache":
|
|
37
|
+
storagePath = base + "/externalCache";
|
|
38
|
+
break;
|
|
39
|
+
case "externalMedia":
|
|
40
|
+
storagePath = base + "/externalMedia";
|
|
41
|
+
break;
|
|
42
|
+
case "appData":
|
|
43
|
+
storagePath = base + "/appData";
|
|
44
|
+
break;
|
|
45
|
+
case "appFiles":
|
|
46
|
+
storagePath = base + "/appFiles";
|
|
47
|
+
break;
|
|
48
|
+
case "appCache":
|
|
49
|
+
storagePath = base + "/appCache";
|
|
50
|
+
break;
|
|
51
|
+
default:
|
|
52
|
+
throw new Error("Unknown storage type: " + options.type);
|
|
53
|
+
}
|
|
54
|
+
await this._fs.ensureDir(storagePath);
|
|
55
|
+
return { path: storagePath };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Return the Blob URL of a file.
|
|
60
|
+
* @warning The returned URI must be released by calling `URL.revokeObjectURL(uri)` after use.
|
|
61
|
+
* Failure to release may cause memory leaks.
|
|
62
|
+
*/
|
|
63
|
+
async getFileUri(options: { path: string }): Promise<{ uri: string }> {
|
|
64
|
+
const entry = await this._fs.getEntry(options.path);
|
|
65
|
+
if (!entry || entry.kind !== "file" || entry.dataBase64 == null) {
|
|
66
|
+
throw new Error("File not found: " + options.path);
|
|
67
|
+
}
|
|
68
|
+
const bytes = bytesFromBase64(entry.dataBase64);
|
|
69
|
+
const blob = new Blob([bytes as BlobPart]);
|
|
70
|
+
return { uri: URL.createObjectURL(blob) };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async writeFile(options: {
|
|
74
|
+
path: string;
|
|
75
|
+
data: string;
|
|
76
|
+
encoding?: "utf8" | "base64";
|
|
77
|
+
}): Promise<void> {
|
|
78
|
+
const idx = options.path.lastIndexOf("/");
|
|
79
|
+
const dir = idx === -1 ? "." : options.path.substring(0, idx) || "/";
|
|
80
|
+
await this._fs.ensureDir(dir);
|
|
81
|
+
const dataBase64 =
|
|
82
|
+
options.encoding === "base64"
|
|
83
|
+
? options.data
|
|
84
|
+
: bytesToBase64(this._textEncoder.encode(options.data));
|
|
85
|
+
await this._fs.putEntry({ path: options.path, kind: "file", dataBase64 });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async readFile(options: {
|
|
89
|
+
path: string;
|
|
90
|
+
encoding?: "utf8" | "base64";
|
|
91
|
+
}): Promise<{ data: string }> {
|
|
92
|
+
const entry = await this._fs.getEntry(options.path);
|
|
93
|
+
if (!entry || entry.kind !== "file" || entry.dataBase64 == null) {
|
|
94
|
+
throw new Error("File not found: " + options.path);
|
|
95
|
+
}
|
|
96
|
+
const data =
|
|
97
|
+
options.encoding === "base64"
|
|
98
|
+
? entry.dataBase64
|
|
99
|
+
: this._textDecoder.decode(bytesFromBase64(entry.dataBase64));
|
|
100
|
+
return { data };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async remove(options: { path: string }): Promise<void> {
|
|
104
|
+
const ok = await this._fs.deleteByPrefix(options.path);
|
|
105
|
+
if (!ok) {
|
|
106
|
+
throw new Error("Deletion failed");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async mkdir(options: { path: string }): Promise<void> {
|
|
111
|
+
await this._fs.ensureDir(options.path);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async exists(options: { path: string }): Promise<{ exists: boolean }> {
|
|
115
|
+
const entry = await this._fs.getEntry(options.path);
|
|
116
|
+
return { exists: !!entry };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -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,107 @@
|
|
|
1
|
+
import type { IFileInfo } from "../IFileSystemPlugin";
|
|
2
|
+
import { IndexedDbStore } from "./IndexedDbStore";
|
|
3
|
+
|
|
4
|
+
interface FsEntry {
|
|
5
|
+
path: string;
|
|
6
|
+
kind: "file" | "dir";
|
|
7
|
+
dataBase64?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class VirtualFileSystem {
|
|
11
|
+
private readonly _STORE_NAME = "entries";
|
|
12
|
+
private readonly _db: IndexedDbStore;
|
|
13
|
+
|
|
14
|
+
constructor(dbName: string) {
|
|
15
|
+
this._db = new IndexedDbStore(dbName, 1, [{ name: this._STORE_NAME, keyPath: "path" }]);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async getEntry(filePath: string): Promise<FsEntry | undefined> {
|
|
19
|
+
return this._db.get<FsEntry>(this._STORE_NAME, filePath);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async putEntry(entry: FsEntry): Promise<void> {
|
|
23
|
+
return this._db.put(this._STORE_NAME, entry);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async deleteByPrefix(pathPrefix: string): Promise<boolean> {
|
|
27
|
+
return this._db.withStore(this._STORE_NAME, "readwrite", async (store) => {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const req = store.openCursor();
|
|
30
|
+
let found = false;
|
|
31
|
+
req.onsuccess = () => {
|
|
32
|
+
const cursor = req.result;
|
|
33
|
+
if (!cursor) {
|
|
34
|
+
resolve(found);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const key = String(cursor.key);
|
|
38
|
+
if (key === pathPrefix || key.startsWith(pathPrefix + "/")) {
|
|
39
|
+
found = true;
|
|
40
|
+
cursor.delete();
|
|
41
|
+
}
|
|
42
|
+
cursor.continue();
|
|
43
|
+
};
|
|
44
|
+
req.onerror = () => reject(req.error);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Return the direct children of a directory.
|
|
51
|
+
* @param dirPath Directory path to query
|
|
52
|
+
* @returns List of child files/directories
|
|
53
|
+
* @note Implicit directory handling: Even when only file paths exist without directory entries,
|
|
54
|
+
* intermediate paths are treated as directories. e.g., With only "/a/b/c.txt" stored,
|
|
55
|
+
* calling listChildren("/a") returns "b" with isDirectory: true.
|
|
56
|
+
*/
|
|
57
|
+
async listChildren(dirPath: string): Promise<IFileInfo[]> {
|
|
58
|
+
const prefix = dirPath === "/" ? "/" : dirPath + "/";
|
|
59
|
+
return this._db.withStore(this._STORE_NAME, "readonly", async (store) => {
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
const req = store.openCursor();
|
|
62
|
+
const map = new Map<string, boolean>();
|
|
63
|
+
req.onsuccess = () => {
|
|
64
|
+
const cursor = req.result;
|
|
65
|
+
if (!cursor) {
|
|
66
|
+
resolve(
|
|
67
|
+
Array.from(map.entries()).map(([name, isDirectory]) => ({ name, isDirectory })),
|
|
68
|
+
);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const key = String(cursor.key);
|
|
72
|
+
if (key.startsWith(prefix)) {
|
|
73
|
+
const rest = key.slice(prefix.length);
|
|
74
|
+
if (rest) {
|
|
75
|
+
const segments = rest.split("/").filter(Boolean);
|
|
76
|
+
if (segments.length > 0) {
|
|
77
|
+
const firstSeg = segments[0];
|
|
78
|
+
if (!map.has(firstSeg)) {
|
|
79
|
+
const isDir = segments.length > 1 || (cursor.value as FsEntry).kind === "dir";
|
|
80
|
+
map.set(firstSeg, isDir);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
cursor.continue();
|
|
86
|
+
};
|
|
87
|
+
req.onerror = () => reject(req.error);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async ensureDir(dirPath: string): Promise<void> {
|
|
93
|
+
if (dirPath === "/") {
|
|
94
|
+
await this.putEntry({ 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(acc);
|
|
102
|
+
if (!existing) {
|
|
103
|
+
await this.putEntry({ path: acc, kind: "dir" });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|