@simplysm/capacitor-plugin-file-system 13.0.68 → 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 CHANGED
@@ -1,241 +1,22 @@
1
1
  # @simplysm/capacitor-plugin-file-system
2
2
 
3
- A Capacitor-based file system access plugin. On Android, it provides direct access to the native file system, while in web environments it operates as an IndexedDB-based virtual file system.
3
+ Simplysm Package - Capacitor File System Plugin
4
4
 
5
5
  ## Installation
6
6
 
7
- ```bash
8
- npm install @simplysm/capacitor-plugin-file-system
9
- npx cap sync
10
- ```
7
+ pnpm add @simplysm/capacitor-plugin-file-system
11
8
 
12
- ## Supported Platforms
9
+ **Peer Dependencies:** `@capacitor/core ^7.4.4`
13
10
 
14
- | Platform | Supported | Implementation |
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
- ### Android Permissions
13
+ ### File System
21
14
 
22
- Different permission models are used depending on the Android version.
23
-
24
- | Android Version | Permission | Behavior |
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
- MIT
22
+ Apache-2.0
@@ -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+: MANAGE_EXTERNAL_STORAGE 권한으로 전체 파일 시스템 접근
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: 외부 저장소 루트 (Environment.getExternalStorageDirectory)
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
- * 파일 URI 얻기 (FileProvider)
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
- * 파일 읽기 (UTF-8 문자열)
45
+ * Read file (UTF-8 string)
46
46
  */
47
47
  static readFileString(filePath: string): Promise<string>;
48
48
  /**
49
- * 파일 읽기 (Bytes)
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
  }
@@ -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: 외부 저장소 루트 (Environment.getExternalStorageDirectory)
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
- * 파일 URI 얻기 (FileProvider)
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
- * 파일 읽기 (UTF-8 문자열)
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
- * 파일 읽기 (Bytes)
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
- * 파일의 Blob URL 반환합니다.
23
- * @warning 반환된 URI 사용 반드시 `URL.revokeObjectURL(uri)`를 호출하여 해제해야 합니다.
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
- * 파일의 Blob URL 반환합니다.
54
- * @warning 반환된 URI 사용 반드시 `URL.revokeObjectURL(uri)`를 호출하여 해제해야 합니다.
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
- * 중간 경로는 디렉토리로 판정됩니다. 예: "/a/b/c.txt" 저장된 상태에서
20
- * listChildren("/a") 호출 "b" isDirectory: true로 반환됩니다.
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
- * 중간 경로는 디렉토리로 판정됩니다. 예: "/a/b/c.txt" 저장된 상태에서
42
- * listChildren("/a") 호출 "b" isDirectory: true로 반환됩니다.
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.68",
4
- "description": "심플리즘 패키지 - Capacitor File System Plugin",
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.68"
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,3 @@
1
+ // File System
2
+ export * from "./FileSystem";
3
+ export * from "./IFileSystemPlugin";
@@ -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
+ }