@simplysm/capacitor-plugin-file-system 12.15.69
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/android/build.gradle +9 -0
- package/android/src/main/AndroidManifest.xml +23 -0
- package/android/src/main/java/kr/co/simplysm/capacitor/filesystem/FileSystemPlugin.java +289 -0
- package/android/src/main/java/kr/co/simplysm/capacitor/filesystem/FileSystemProvider.java +6 -0
- package/android/src/main/res/xml/file_provider_paths.xml +8 -0
- package/dist/FileSystem.d.ts +63 -0
- package/dist/FileSystem.js +111 -0
- package/dist/IFileSystemPlugin.d.ts +48 -0
- package/dist/IFileSystemPlugin.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/web/FileSystemWeb.d.ts +46 -0
- package/dist/web/FileSystemWeb.js +91 -0
- package/dist/web/VirtualFileSystem.d.ts +20 -0
- package/dist/web/VirtualFileSystem.js +119 -0
- package/package.json +23 -0
- package/src/FileSystem.ts +123 -0
- package/src/IFileSystemPlugin.ts +26 -0
- package/src/index.ts +4 -0
- package/src/web/FileSystemWeb.ts +107 -0
- package/src/web/VirtualFileSystem.ts +138 -0
- package/tsconfig.json +8 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
3
|
+
xmlns:tools="http://schemas.android.com/tools">
|
|
4
|
+
|
|
5
|
+
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
|
6
|
+
android:maxSdkVersion="32"/>
|
|
7
|
+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
|
8
|
+
android:maxSdkVersion="29"/>
|
|
9
|
+
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
|
10
|
+
tools:ignore="ScopedStorage"/>
|
|
11
|
+
|
|
12
|
+
<application>
|
|
13
|
+
<provider
|
|
14
|
+
android:name=".FileSystemProvider"
|
|
15
|
+
android:authorities="${applicationId}.filesystem.provider"
|
|
16
|
+
android:exported="false"
|
|
17
|
+
android:grantUriPermissions="true">
|
|
18
|
+
<meta-data
|
|
19
|
+
android:name="android.support.FILE_PROVIDER_PATHS"
|
|
20
|
+
android:resource="@xml/file_provider_paths"/>
|
|
21
|
+
</provider>
|
|
22
|
+
</application>
|
|
23
|
+
</manifest>
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
package kr.co.simplysm.capacitor.filesystem;
|
|
2
|
+
|
|
3
|
+
import android.Manifest;
|
|
4
|
+
import android.content.Context;
|
|
5
|
+
import android.content.Intent;
|
|
6
|
+
import android.content.pm.PackageManager;
|
|
7
|
+
import android.net.Uri;
|
|
8
|
+
import android.os.Build;
|
|
9
|
+
import android.os.Environment;
|
|
10
|
+
import android.provider.Settings;
|
|
11
|
+
import android.util.Base64;
|
|
12
|
+
import android.util.Log;
|
|
13
|
+
|
|
14
|
+
import androidx.core.content.ContextCompat;
|
|
15
|
+
import androidx.core.content.FileProvider;
|
|
16
|
+
|
|
17
|
+
import com.getcapacitor.JSArray;
|
|
18
|
+
import com.getcapacitor.JSObject;
|
|
19
|
+
import com.getcapacitor.Plugin;
|
|
20
|
+
import com.getcapacitor.PluginCall;
|
|
21
|
+
import com.getcapacitor.PluginMethod;
|
|
22
|
+
import com.getcapacitor.annotation.CapacitorPlugin;
|
|
23
|
+
|
|
24
|
+
import java.io.*;
|
|
25
|
+
import java.nio.charset.StandardCharsets;
|
|
26
|
+
|
|
27
|
+
@CapacitorPlugin(name = "FileSystem")
|
|
28
|
+
public class FileSystemPlugin extends Plugin {
|
|
29
|
+
|
|
30
|
+
private static final String TAG = "FileSystemPlugin";
|
|
31
|
+
|
|
32
|
+
@PluginMethod
|
|
33
|
+
public void checkPermission(PluginCall call) {
|
|
34
|
+
boolean granted;
|
|
35
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
36
|
+
granted = Environment.isExternalStorageManager();
|
|
37
|
+
} else {
|
|
38
|
+
Context ctx = getContext();
|
|
39
|
+
granted = ContextCompat.checkSelfPermission(ctx, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
|
|
40
|
+
&& ContextCompat.checkSelfPermission(ctx, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
|
|
41
|
+
}
|
|
42
|
+
JSObject ret = new JSObject();
|
|
43
|
+
ret.put("granted", granted);
|
|
44
|
+
call.resolve(ret);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@PluginMethod
|
|
48
|
+
public void requestPermission(PluginCall call) {
|
|
49
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
50
|
+
if (!Environment.isExternalStorageManager()) {
|
|
51
|
+
Intent intent = new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION);
|
|
52
|
+
intent.setData(Uri.parse("package:" + getContext().getPackageName()));
|
|
53
|
+
getActivity().startActivity(intent);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
call.resolve();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@PluginMethod
|
|
60
|
+
public void readdir(PluginCall call) {
|
|
61
|
+
String path = call.getString("path");
|
|
62
|
+
if (path == null) {
|
|
63
|
+
call.reject("path is required");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
File dir = new File(path);
|
|
68
|
+
if (!dir.exists() || !dir.isDirectory()) {
|
|
69
|
+
call.reject("Directory does not exist");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
File[] files = dir.listFiles();
|
|
74
|
+
if (files == null) {
|
|
75
|
+
call.reject("Cannot read directory");
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
JSArray result = new JSArray();
|
|
80
|
+
for (File f : files) {
|
|
81
|
+
JSObject info = new JSObject();
|
|
82
|
+
info.put("name", f.getName());
|
|
83
|
+
info.put("isDirectory", f.isDirectory());
|
|
84
|
+
result.put(info);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
JSObject ret = new JSObject();
|
|
88
|
+
ret.put("files", result);
|
|
89
|
+
call.resolve(ret);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@PluginMethod
|
|
93
|
+
public void getStoragePath(PluginCall call) {
|
|
94
|
+
String type = call.getString("type");
|
|
95
|
+
if (type == null) {
|
|
96
|
+
call.reject("type is required");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
Context ctx = getContext();
|
|
101
|
+
File path;
|
|
102
|
+
|
|
103
|
+
switch (type) {
|
|
104
|
+
case "external":
|
|
105
|
+
path = Environment.getExternalStorageDirectory();
|
|
106
|
+
break;
|
|
107
|
+
case "externalFiles":
|
|
108
|
+
path = ctx.getExternalFilesDir(null);
|
|
109
|
+
break;
|
|
110
|
+
case "externalCache":
|
|
111
|
+
path = ctx.getExternalCacheDir();
|
|
112
|
+
break;
|
|
113
|
+
case "externalMedia":
|
|
114
|
+
File[] dirs = ctx.getExternalMediaDirs();
|
|
115
|
+
path = (dirs.length > 0) ? dirs[0] : null;
|
|
116
|
+
break;
|
|
117
|
+
case "appData":
|
|
118
|
+
path = new File(ctx.getApplicationInfo().dataDir);
|
|
119
|
+
break;
|
|
120
|
+
case "appFiles":
|
|
121
|
+
path = ctx.getFilesDir();
|
|
122
|
+
break;
|
|
123
|
+
case "appCache":
|
|
124
|
+
path = ctx.getCacheDir();
|
|
125
|
+
break;
|
|
126
|
+
default:
|
|
127
|
+
call.reject("Unknown type: " + type);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (path == null) {
|
|
132
|
+
call.reject("Path not available");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
JSObject ret = new JSObject();
|
|
137
|
+
ret.put("path", path.getAbsolutePath());
|
|
138
|
+
call.resolve(ret);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@PluginMethod
|
|
142
|
+
public void getFileUri(PluginCall call) {
|
|
143
|
+
String path = call.getString("path");
|
|
144
|
+
if (path == null) {
|
|
145
|
+
call.reject("path is required");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
String authority = getContext().getPackageName() + ".filesystem.provider";
|
|
151
|
+
Uri uri = FileProvider.getUriForFile(getContext(), authority, new File(path));
|
|
152
|
+
JSObject ret = new JSObject();
|
|
153
|
+
ret.put("uri", uri.toString());
|
|
154
|
+
call.resolve(ret);
|
|
155
|
+
} catch (Exception e) {
|
|
156
|
+
Log.e(TAG, "getFileUri failed", e);
|
|
157
|
+
call.reject("getFileUri failed: " + e.getMessage());
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@PluginMethod
|
|
162
|
+
public void writeFile(PluginCall call) {
|
|
163
|
+
String path = call.getString("path");
|
|
164
|
+
String data = call.getString("data");
|
|
165
|
+
String encoding = call.getString("encoding", "utf8");
|
|
166
|
+
|
|
167
|
+
if (path == null || data == null) {
|
|
168
|
+
call.reject("path and data are required");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
File file = new File(path);
|
|
174
|
+
File parent = file.getParentFile();
|
|
175
|
+
if (parent != null && !parent.exists()) {
|
|
176
|
+
parent.mkdirs();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
byte[] bytes = "base64".equals(encoding)
|
|
180
|
+
? Base64.decode(data, Base64.DEFAULT)
|
|
181
|
+
: data.getBytes(StandardCharsets.UTF_8);
|
|
182
|
+
|
|
183
|
+
try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file))) {
|
|
184
|
+
bos.write(bytes);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
call.resolve();
|
|
188
|
+
} catch (Exception e) {
|
|
189
|
+
Log.e(TAG, "writeFile failed", e);
|
|
190
|
+
call.reject("Write failed: " + e.getMessage());
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
@PluginMethod
|
|
195
|
+
public void readFile(PluginCall call) {
|
|
196
|
+
String path = call.getString("path");
|
|
197
|
+
String encoding = call.getString("encoding", "utf8");
|
|
198
|
+
|
|
199
|
+
if (path == null) {
|
|
200
|
+
call.reject("path is required");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
File file = new File(path);
|
|
205
|
+
if (!file.exists()) {
|
|
206
|
+
call.reject("File not found: " + path);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
|
|
211
|
+
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
|
|
212
|
+
|
|
213
|
+
byte[] buf = new byte[8192];
|
|
214
|
+
int len;
|
|
215
|
+
while ((len = bis.read(buf)) != -1) {
|
|
216
|
+
baos.write(buf, 0, len);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
String result = "base64".equals(encoding)
|
|
220
|
+
? Base64.encodeToString(baos.toByteArray(), Base64.NO_WRAP)
|
|
221
|
+
: baos.toString("UTF-8");
|
|
222
|
+
|
|
223
|
+
JSObject ret = new JSObject();
|
|
224
|
+
ret.put("data", result);
|
|
225
|
+
call.resolve(ret);
|
|
226
|
+
} catch (Exception e) {
|
|
227
|
+
Log.e(TAG, "readFile failed", e);
|
|
228
|
+
call.reject("Read failed: " + e.getMessage());
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
@PluginMethod
|
|
233
|
+
public void remove(PluginCall call) {
|
|
234
|
+
String path = call.getString("path");
|
|
235
|
+
if (path == null) {
|
|
236
|
+
call.reject("path is required");
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (deleteRecursively(new File(path))) {
|
|
241
|
+
call.resolve();
|
|
242
|
+
} else {
|
|
243
|
+
call.reject("Delete failed");
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
@PluginMethod
|
|
248
|
+
public void mkdir(PluginCall call) {
|
|
249
|
+
String path = call.getString("path");
|
|
250
|
+
if (path == null) {
|
|
251
|
+
call.reject("path is required");
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
File dir = new File(path);
|
|
256
|
+
if (dir.exists() || dir.mkdirs()) {
|
|
257
|
+
call.resolve();
|
|
258
|
+
} else {
|
|
259
|
+
call.reject("Failed to create directory");
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
@PluginMethod
|
|
264
|
+
public void exists(PluginCall call) {
|
|
265
|
+
String path = call.getString("path");
|
|
266
|
+
if (path == null) {
|
|
267
|
+
call.reject("path is required");
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
JSObject ret = new JSObject();
|
|
272
|
+
ret.put("exists", new File(path).exists());
|
|
273
|
+
call.resolve(ret);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private boolean deleteRecursively(File file) {
|
|
277
|
+
if (file.isDirectory()) {
|
|
278
|
+
File[] children = file.listFiles();
|
|
279
|
+
if (children != null) {
|
|
280
|
+
for (File child : children) {
|
|
281
|
+
if (!deleteRecursively(child)) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return file.delete();
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<paths>
|
|
3
|
+
<external-path name="external" path="." />
|
|
4
|
+
<external-files-path name="external_files" path="." />
|
|
5
|
+
<external-cache-path name="external_cache" path="." />
|
|
6
|
+
<files-path name="files" path="." />
|
|
7
|
+
<cache-path name="cache" path="." />
|
|
8
|
+
</paths>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { IFileInfo, TStorage } from "./IFileSystemPlugin";
|
|
2
|
+
/**
|
|
3
|
+
* 파일 시스템 접근 플러그인
|
|
4
|
+
* - Android 11+: MANAGE_EXTERNAL_STORAGE 권한으로 전체 파일 시스템 접근
|
|
5
|
+
* - Android 10-: READ/WRITE_EXTERNAL_STORAGE 권한
|
|
6
|
+
* - Browser: IndexedDB 기반 에뮬레이션
|
|
7
|
+
*/
|
|
8
|
+
export declare abstract class FileSystem {
|
|
9
|
+
/**
|
|
10
|
+
* 권한 확인
|
|
11
|
+
*/
|
|
12
|
+
static checkPermissionAsync(): Promise<boolean>;
|
|
13
|
+
/**
|
|
14
|
+
* 권한 요청
|
|
15
|
+
* - Android 11+: 설정 화면으로 이동
|
|
16
|
+
* - Android 10-: 권한 다이얼로그 표시
|
|
17
|
+
*/
|
|
18
|
+
static requestPermissionAsync(): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* 디렉토리 읽기
|
|
21
|
+
*/
|
|
22
|
+
static readdirAsync(dirPath: string): Promise<IFileInfo[]>;
|
|
23
|
+
/**
|
|
24
|
+
* 저장소 경로 얻기
|
|
25
|
+
* @param type 저장소 타입
|
|
26
|
+
* - external: 외부 저장소 루트 (Environment.getExternalStorageDirectory)
|
|
27
|
+
* - externalFiles: 앱 전용 외부 파일 디렉토리
|
|
28
|
+
* - externalCache: 앱 전용 외부 캐시 디렉토리
|
|
29
|
+
* - externalMedia: 앱 전용 외부 미디어 디렉토리
|
|
30
|
+
* - appData: 앱 데이터 디렉토리
|
|
31
|
+
* - appFiles: 앱 파일 디렉토리
|
|
32
|
+
* - appCache: 앱 캐시 디렉토리
|
|
33
|
+
*/
|
|
34
|
+
static getStoragePathAsync(type: TStorage): Promise<string>;
|
|
35
|
+
/**
|
|
36
|
+
* 파일 URI 얻기 (FileProvider)
|
|
37
|
+
*/
|
|
38
|
+
static getFileUriAsync(filePath: string): Promise<string>;
|
|
39
|
+
/**
|
|
40
|
+
* 파일 쓰기
|
|
41
|
+
*/
|
|
42
|
+
static writeFileAsync(filePath: string, data: string | Buffer): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* 파일 읽기 (UTF-8 문자열)
|
|
45
|
+
*/
|
|
46
|
+
static readFileStringAsync(filePath: string): Promise<string>;
|
|
47
|
+
/**
|
|
48
|
+
* 파일 읽기 (Buffer)
|
|
49
|
+
*/
|
|
50
|
+
static readFileBufferAsync(filePath: string): Promise<Buffer>;
|
|
51
|
+
/**
|
|
52
|
+
* 파일/디렉토리 삭제 (재귀)
|
|
53
|
+
*/
|
|
54
|
+
static removeAsync(targetPath: string): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* 디렉토리 생성 (재귀)
|
|
57
|
+
*/
|
|
58
|
+
static mkdirsAsync(targetPath: string): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* 존재 여부 확인
|
|
61
|
+
*/
|
|
62
|
+
static existsAsync(targetPath: string): Promise<boolean>;
|
|
63
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { registerPlugin } from "@capacitor/core";
|
|
2
|
+
const FileSystemPlugin = registerPlugin("FileSystem", {
|
|
3
|
+
web: async () => {
|
|
4
|
+
const { FileSystemWeb } = await import("./web/FileSystemWeb");
|
|
5
|
+
return new FileSystemWeb();
|
|
6
|
+
},
|
|
7
|
+
});
|
|
8
|
+
/**
|
|
9
|
+
* 파일 시스템 접근 플러그인
|
|
10
|
+
* - Android 11+: MANAGE_EXTERNAL_STORAGE 권한으로 전체 파일 시스템 접근
|
|
11
|
+
* - Android 10-: READ/WRITE_EXTERNAL_STORAGE 권한
|
|
12
|
+
* - Browser: IndexedDB 기반 에뮬레이션
|
|
13
|
+
*/
|
|
14
|
+
export class FileSystem {
|
|
15
|
+
/**
|
|
16
|
+
* 권한 확인
|
|
17
|
+
*/
|
|
18
|
+
static async checkPermissionAsync() {
|
|
19
|
+
const result = await FileSystemPlugin.checkPermission();
|
|
20
|
+
return result.granted;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 권한 요청
|
|
24
|
+
* - Android 11+: 설정 화면으로 이동
|
|
25
|
+
* - Android 10-: 권한 다이얼로그 표시
|
|
26
|
+
*/
|
|
27
|
+
static async requestPermissionAsync() {
|
|
28
|
+
await FileSystemPlugin.requestPermission();
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 디렉토리 읽기
|
|
32
|
+
*/
|
|
33
|
+
static async readdirAsync(dirPath) {
|
|
34
|
+
const result = await FileSystemPlugin.readdir({ path: dirPath });
|
|
35
|
+
return result.files;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 저장소 경로 얻기
|
|
39
|
+
* @param type 저장소 타입
|
|
40
|
+
* - external: 외부 저장소 루트 (Environment.getExternalStorageDirectory)
|
|
41
|
+
* - externalFiles: 앱 전용 외부 파일 디렉토리
|
|
42
|
+
* - externalCache: 앱 전용 외부 캐시 디렉토리
|
|
43
|
+
* - externalMedia: 앱 전용 외부 미디어 디렉토리
|
|
44
|
+
* - appData: 앱 데이터 디렉토리
|
|
45
|
+
* - appFiles: 앱 파일 디렉토리
|
|
46
|
+
* - appCache: 앱 캐시 디렉토리
|
|
47
|
+
*/
|
|
48
|
+
static async getStoragePathAsync(type) {
|
|
49
|
+
const result = await FileSystemPlugin.getStoragePath({ type });
|
|
50
|
+
return result.path;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 파일 URI 얻기 (FileProvider)
|
|
54
|
+
*/
|
|
55
|
+
static async getFileUriAsync(filePath) {
|
|
56
|
+
const result = await FileSystemPlugin.getFileUri({ path: filePath });
|
|
57
|
+
return result.uri;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 파일 쓰기
|
|
61
|
+
*/
|
|
62
|
+
static async writeFileAsync(filePath, data) {
|
|
63
|
+
if (Buffer.isBuffer(data)) {
|
|
64
|
+
await FileSystemPlugin.writeFile({
|
|
65
|
+
path: filePath,
|
|
66
|
+
data: data.toString("base64"),
|
|
67
|
+
encoding: "base64",
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
await FileSystemPlugin.writeFile({
|
|
72
|
+
path: filePath,
|
|
73
|
+
data,
|
|
74
|
+
encoding: "utf8",
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 파일 읽기 (UTF-8 문자열)
|
|
80
|
+
*/
|
|
81
|
+
static async readFileStringAsync(filePath) {
|
|
82
|
+
const result = await FileSystemPlugin.readFile({ path: filePath, encoding: "utf8" });
|
|
83
|
+
return result.data;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 파일 읽기 (Buffer)
|
|
87
|
+
*/
|
|
88
|
+
static async readFileBufferAsync(filePath) {
|
|
89
|
+
const result = await FileSystemPlugin.readFile({ path: filePath, encoding: "base64" });
|
|
90
|
+
return Buffer.from(result.data, "base64");
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 파일/디렉토리 삭제 (재귀)
|
|
94
|
+
*/
|
|
95
|
+
static async removeAsync(targetPath) {
|
|
96
|
+
await FileSystemPlugin.remove({ path: targetPath });
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* 디렉토리 생성 (재귀)
|
|
100
|
+
*/
|
|
101
|
+
static async mkdirsAsync(targetPath) {
|
|
102
|
+
await FileSystemPlugin.mkdir({ path: targetPath });
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 존재 여부 확인
|
|
106
|
+
*/
|
|
107
|
+
static async existsAsync(targetPath) {
|
|
108
|
+
const result = await FileSystemPlugin.exists({ path: targetPath });
|
|
109
|
+
return result.exists;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export type TStorage = "external" | "externalFiles" | "externalCache" | "externalMedia" | "appData" | "appFiles" | "appCache";
|
|
2
|
+
export interface IFileInfo {
|
|
3
|
+
name: string;
|
|
4
|
+
isDirectory: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface IFileSystemPlugin {
|
|
7
|
+
checkPermission(): Promise<{
|
|
8
|
+
granted: boolean;
|
|
9
|
+
}>;
|
|
10
|
+
requestPermission(): Promise<void>;
|
|
11
|
+
readdir(options: {
|
|
12
|
+
path: string;
|
|
13
|
+
}): Promise<{
|
|
14
|
+
files: IFileInfo[];
|
|
15
|
+
}>;
|
|
16
|
+
getStoragePath(options: {
|
|
17
|
+
type: TStorage;
|
|
18
|
+
}): Promise<{
|
|
19
|
+
path: string;
|
|
20
|
+
}>;
|
|
21
|
+
getFileUri(options: {
|
|
22
|
+
path: string;
|
|
23
|
+
}): Promise<{
|
|
24
|
+
uri: string;
|
|
25
|
+
}>;
|
|
26
|
+
writeFile(options: {
|
|
27
|
+
path: string;
|
|
28
|
+
data: string;
|
|
29
|
+
encoding?: "utf8" | "base64";
|
|
30
|
+
}): Promise<void>;
|
|
31
|
+
readFile(options: {
|
|
32
|
+
path: string;
|
|
33
|
+
encoding?: "utf8" | "base64";
|
|
34
|
+
}): Promise<{
|
|
35
|
+
data: string;
|
|
36
|
+
}>;
|
|
37
|
+
remove(options: {
|
|
38
|
+
path: string;
|
|
39
|
+
}): Promise<void>;
|
|
40
|
+
mkdir(options: {
|
|
41
|
+
path: string;
|
|
42
|
+
}): Promise<void>;
|
|
43
|
+
exists(options: {
|
|
44
|
+
path: string;
|
|
45
|
+
}): Promise<{
|
|
46
|
+
exists: boolean;
|
|
47
|
+
}>;
|
|
48
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { WebPlugin } from "@capacitor/core";
|
|
2
|
+
import { IFileInfo, IFileSystemPlugin, TStorage } from "../IFileSystemPlugin";
|
|
3
|
+
export declare class FileSystemWeb extends WebPlugin implements IFileSystemPlugin {
|
|
4
|
+
private readonly _fs;
|
|
5
|
+
checkPermission(): Promise<{
|
|
6
|
+
granted: boolean;
|
|
7
|
+
}>;
|
|
8
|
+
requestPermission(): Promise<void>;
|
|
9
|
+
readdir(options: {
|
|
10
|
+
path: string;
|
|
11
|
+
}): Promise<{
|
|
12
|
+
files: IFileInfo[];
|
|
13
|
+
}>;
|
|
14
|
+
getStoragePath(options: {
|
|
15
|
+
type: TStorage;
|
|
16
|
+
}): Promise<{
|
|
17
|
+
path: string;
|
|
18
|
+
}>;
|
|
19
|
+
getFileUri(options: {
|
|
20
|
+
path: string;
|
|
21
|
+
}): Promise<{
|
|
22
|
+
uri: string;
|
|
23
|
+
}>;
|
|
24
|
+
writeFile(options: {
|
|
25
|
+
path: string;
|
|
26
|
+
data: string;
|
|
27
|
+
encoding?: "utf8" | "base64";
|
|
28
|
+
}): Promise<void>;
|
|
29
|
+
readFile(options: {
|
|
30
|
+
path: string;
|
|
31
|
+
encoding?: "utf8" | "base64";
|
|
32
|
+
}): Promise<{
|
|
33
|
+
data: string;
|
|
34
|
+
}>;
|
|
35
|
+
remove(options: {
|
|
36
|
+
path: string;
|
|
37
|
+
}): Promise<void>;
|
|
38
|
+
mkdir(options: {
|
|
39
|
+
path: string;
|
|
40
|
+
}): Promise<void>;
|
|
41
|
+
exists(options: {
|
|
42
|
+
path: string;
|
|
43
|
+
}): Promise<{
|
|
44
|
+
exists: boolean;
|
|
45
|
+
}>;
|
|
46
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { WebPlugin } from "@capacitor/core";
|
|
2
|
+
import { VirtualFileSystem } from "./VirtualFileSystem";
|
|
3
|
+
import path from "path";
|
|
4
|
+
export class FileSystemWeb extends WebPlugin {
|
|
5
|
+
constructor() {
|
|
6
|
+
super(...arguments);
|
|
7
|
+
this._fs = new VirtualFileSystem("capacitor_web_virtual_fs");
|
|
8
|
+
}
|
|
9
|
+
async checkPermission() {
|
|
10
|
+
return await Promise.resolve({ granted: true });
|
|
11
|
+
}
|
|
12
|
+
async requestPermission() { }
|
|
13
|
+
async readdir(options) {
|
|
14
|
+
const entry = await this._fs.getEntry(options.path);
|
|
15
|
+
if (!entry || entry.kind !== "dir") {
|
|
16
|
+
throw new Error("Directory does not exist");
|
|
17
|
+
}
|
|
18
|
+
const files = await this._fs.listChildren(options.path);
|
|
19
|
+
return { files };
|
|
20
|
+
}
|
|
21
|
+
async getStoragePath(options) {
|
|
22
|
+
const base = "/webfs";
|
|
23
|
+
let storagePath;
|
|
24
|
+
switch (options.type) {
|
|
25
|
+
case "external":
|
|
26
|
+
storagePath = base + "/external";
|
|
27
|
+
break;
|
|
28
|
+
case "externalFiles":
|
|
29
|
+
storagePath = base + "/externalFiles";
|
|
30
|
+
break;
|
|
31
|
+
case "externalCache":
|
|
32
|
+
storagePath = base + "/externalCache";
|
|
33
|
+
break;
|
|
34
|
+
case "externalMedia":
|
|
35
|
+
storagePath = base + "/externalMedia";
|
|
36
|
+
break;
|
|
37
|
+
case "appData":
|
|
38
|
+
storagePath = base + "/appData";
|
|
39
|
+
break;
|
|
40
|
+
case "appFiles":
|
|
41
|
+
storagePath = base + "/appFiles";
|
|
42
|
+
break;
|
|
43
|
+
case "appCache":
|
|
44
|
+
storagePath = base + "/appCache";
|
|
45
|
+
break;
|
|
46
|
+
default:
|
|
47
|
+
throw new Error("Unknown storage type: " + options.type);
|
|
48
|
+
}
|
|
49
|
+
await this._fs.ensureDir(storagePath);
|
|
50
|
+
return { path: storagePath };
|
|
51
|
+
}
|
|
52
|
+
async getFileUri(options) {
|
|
53
|
+
const entry = await this._fs.getEntry(options.path);
|
|
54
|
+
if (!entry || entry.kind !== "file" || entry.dataBase64 == null) {
|
|
55
|
+
throw new Error("File not found: " + options.path);
|
|
56
|
+
}
|
|
57
|
+
const byteCharacters = atob(entry.dataBase64);
|
|
58
|
+
const byteNumbers = new Array(byteCharacters.length);
|
|
59
|
+
for (let i = 0; i < byteCharacters.length; i++) {
|
|
60
|
+
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
|
61
|
+
}
|
|
62
|
+
const blob = new Blob([new Uint8Array(byteNumbers)]);
|
|
63
|
+
return { uri: URL.createObjectURL(blob) };
|
|
64
|
+
}
|
|
65
|
+
async writeFile(options) {
|
|
66
|
+
await this._fs.ensureDir(path.dirname(options.path));
|
|
67
|
+
const dataBase64 = options.encoding === "base64" ? options.data : btoa(options.data);
|
|
68
|
+
await this._fs.putEntry({ path: options.path, kind: "file", dataBase64 });
|
|
69
|
+
}
|
|
70
|
+
async readFile(options) {
|
|
71
|
+
const entry = await this._fs.getEntry(options.path);
|
|
72
|
+
if (!entry || entry.kind !== "file" || entry.dataBase64 == null) {
|
|
73
|
+
throw new Error("File not found: " + options.path);
|
|
74
|
+
}
|
|
75
|
+
const data = options.encoding === "base64" ? entry.dataBase64 : atob(entry.dataBase64);
|
|
76
|
+
return { data };
|
|
77
|
+
}
|
|
78
|
+
async remove(options) {
|
|
79
|
+
const ok = await this._fs.deleteByPrefix(options.path);
|
|
80
|
+
if (!ok) {
|
|
81
|
+
throw new Error("Deletion failed");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async mkdir(options) {
|
|
85
|
+
await this._fs.ensureDir(options.path);
|
|
86
|
+
}
|
|
87
|
+
async exists(options) {
|
|
88
|
+
const entry = await this._fs.getEntry(options.path);
|
|
89
|
+
return { exists: !!entry };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { IFileInfo } from "../IFileSystemPlugin";
|
|
2
|
+
interface FsEntry {
|
|
3
|
+
path: string;
|
|
4
|
+
kind: "file" | "dir";
|
|
5
|
+
dataBase64?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare class VirtualFileSystem {
|
|
8
|
+
private readonly _dbName;
|
|
9
|
+
private readonly _STORE_NAME;
|
|
10
|
+
private readonly _DB_VERSION;
|
|
11
|
+
constructor(_dbName: string);
|
|
12
|
+
private _openDb;
|
|
13
|
+
private _withStore;
|
|
14
|
+
getEntry(filePath: string): Promise<FsEntry | undefined>;
|
|
15
|
+
putEntry(entry: FsEntry): Promise<void>;
|
|
16
|
+
deleteByPrefix(pathPrefix: string): Promise<boolean>;
|
|
17
|
+
listChildren(dirPath: string): Promise<IFileInfo[]>;
|
|
18
|
+
ensureDir(dirPath: string): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
export class VirtualFileSystem {
|
|
2
|
+
constructor(_dbName) {
|
|
3
|
+
this._dbName = _dbName;
|
|
4
|
+
this._STORE_NAME = "entries";
|
|
5
|
+
this._DB_VERSION = 1;
|
|
6
|
+
}
|
|
7
|
+
async _openDb() {
|
|
8
|
+
return await new Promise((resolve, reject) => {
|
|
9
|
+
const req = indexedDB.open(this._dbName, this._DB_VERSION);
|
|
10
|
+
req.onupgradeneeded = () => {
|
|
11
|
+
const db = req.result;
|
|
12
|
+
if (!db.objectStoreNames.contains(this._STORE_NAME)) {
|
|
13
|
+
db.createObjectStore(this._STORE_NAME, { keyPath: "path" });
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
req.onsuccess = () => resolve(req.result);
|
|
17
|
+
req.onerror = () => reject(req.error);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
async _withStore(mode, fn) {
|
|
21
|
+
const db = await this._openDb();
|
|
22
|
+
return await new Promise((resolve, reject) => {
|
|
23
|
+
const tx = db.transaction(this._STORE_NAME, mode);
|
|
24
|
+
const store = tx.objectStore(this._STORE_NAME);
|
|
25
|
+
let result;
|
|
26
|
+
Promise.resolve(fn(store))
|
|
27
|
+
.then((r) => {
|
|
28
|
+
result = r;
|
|
29
|
+
})
|
|
30
|
+
.catch(reject);
|
|
31
|
+
tx.oncomplete = () => resolve(result);
|
|
32
|
+
tx.onerror = () => reject(tx.error);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
async getEntry(filePath) {
|
|
36
|
+
return await this._withStore("readonly", async (store) => {
|
|
37
|
+
return await new Promise((resolve, reject) => {
|
|
38
|
+
const req = store.get(filePath);
|
|
39
|
+
req.onsuccess = () => resolve(req.result);
|
|
40
|
+
req.onerror = () => reject(req.error);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async putEntry(entry) {
|
|
45
|
+
return await this._withStore("readwrite", async (store) => {
|
|
46
|
+
return await new Promise((resolve, reject) => {
|
|
47
|
+
const req = store.put(entry);
|
|
48
|
+
req.onsuccess = () => resolve();
|
|
49
|
+
req.onerror = () => reject(req.error);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async deleteByPrefix(pathPrefix) {
|
|
54
|
+
return await this._withStore("readwrite", async (store) => {
|
|
55
|
+
return await new Promise((resolve, reject) => {
|
|
56
|
+
const req = store.openCursor();
|
|
57
|
+
let found = false;
|
|
58
|
+
req.onsuccess = () => {
|
|
59
|
+
const cursor = req.result;
|
|
60
|
+
if (!cursor) {
|
|
61
|
+
resolve(found);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const key = String(cursor.key);
|
|
65
|
+
if (key === pathPrefix || key.startsWith(pathPrefix + "/")) {
|
|
66
|
+
found = true;
|
|
67
|
+
cursor.delete();
|
|
68
|
+
}
|
|
69
|
+
cursor.continue();
|
|
70
|
+
};
|
|
71
|
+
req.onerror = () => reject(req.error);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
async listChildren(dirPath) {
|
|
76
|
+
const prefix = dirPath === "/" ? "/" : dirPath + "/";
|
|
77
|
+
return await this._withStore("readonly", async (store) => {
|
|
78
|
+
return await new Promise((resolve, reject) => {
|
|
79
|
+
const req = store.openCursor();
|
|
80
|
+
const map = new Map();
|
|
81
|
+
req.onsuccess = () => {
|
|
82
|
+
const cursor = req.result;
|
|
83
|
+
if (!cursor) {
|
|
84
|
+
resolve(Array.from(map.entries()).map(([name, isDirectory]) => ({ name, isDirectory })));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const key = String(cursor.key);
|
|
88
|
+
if (key.startsWith(prefix)) {
|
|
89
|
+
const rest = key.slice(prefix.length);
|
|
90
|
+
if (rest) {
|
|
91
|
+
const firstSeg = rest.split("/")[0];
|
|
92
|
+
if (firstSeg && !map.has(firstSeg)) {
|
|
93
|
+
const value = cursor.value;
|
|
94
|
+
map.set(firstSeg, value.kind === "dir");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
cursor.continue();
|
|
99
|
+
};
|
|
100
|
+
req.onerror = () => reject(req.error);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
async ensureDir(dirPath) {
|
|
105
|
+
if (dirPath === "/") {
|
|
106
|
+
await this.putEntry({ path: "/", kind: "dir" });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const segments = dirPath.split("/").filter(Boolean);
|
|
110
|
+
let acc = "";
|
|
111
|
+
for (const seg of segments) {
|
|
112
|
+
acc += "/" + seg;
|
|
113
|
+
const existing = await this.getEntry(acc);
|
|
114
|
+
if (!existing) {
|
|
115
|
+
await this.putEntry({ path: acc, kind: "dir" });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@simplysm/capacitor-plugin-file-system",
|
|
3
|
+
"version": "12.15.69",
|
|
4
|
+
"description": "심플리즘 패키지 - Capacitor File System Plugin",
|
|
5
|
+
"author": "김석래",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/kslhunter/simplysm.git",
|
|
9
|
+
"directory": "packages/capacitor-plugin-file-system"
|
|
10
|
+
},
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"capacitor": {
|
|
16
|
+
"android": {
|
|
17
|
+
"src": "android"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@capacitor/core": "^7.4.4"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { registerPlugin } from "@capacitor/core";
|
|
2
|
+
import { IFileInfo, IFileSystemPlugin, TStorage } from "./IFileSystemPlugin";
|
|
3
|
+
|
|
4
|
+
const FileSystemPlugin = registerPlugin<IFileSystemPlugin>("FileSystem", {
|
|
5
|
+
web: async () => {
|
|
6
|
+
const { FileSystemWeb } = await import("./web/FileSystemWeb");
|
|
7
|
+
return new FileSystemWeb();
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 파일 시스템 접근 플러그인
|
|
13
|
+
* - Android 11+: MANAGE_EXTERNAL_STORAGE 권한으로 전체 파일 시스템 접근
|
|
14
|
+
* - Android 10-: READ/WRITE_EXTERNAL_STORAGE 권한
|
|
15
|
+
* - Browser: IndexedDB 기반 에뮬레이션
|
|
16
|
+
*/
|
|
17
|
+
export abstract class FileSystem {
|
|
18
|
+
/**
|
|
19
|
+
* 권한 확인
|
|
20
|
+
*/
|
|
21
|
+
static async checkPermissionAsync(): Promise<boolean> {
|
|
22
|
+
const result = await FileSystemPlugin.checkPermission();
|
|
23
|
+
return result.granted;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 권한 요청
|
|
28
|
+
* - Android 11+: 설정 화면으로 이동
|
|
29
|
+
* - Android 10-: 권한 다이얼로그 표시
|
|
30
|
+
*/
|
|
31
|
+
static async requestPermissionAsync(): Promise<void> {
|
|
32
|
+
await FileSystemPlugin.requestPermission();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 디렉토리 읽기
|
|
37
|
+
*/
|
|
38
|
+
static async readdirAsync(dirPath: string): Promise<IFileInfo[]> {
|
|
39
|
+
const result = await FileSystemPlugin.readdir({ path: dirPath });
|
|
40
|
+
return result.files;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 저장소 경로 얻기
|
|
45
|
+
* @param type 저장소 타입
|
|
46
|
+
* - external: 외부 저장소 루트 (Environment.getExternalStorageDirectory)
|
|
47
|
+
* - externalFiles: 앱 전용 외부 파일 디렉토리
|
|
48
|
+
* - externalCache: 앱 전용 외부 캐시 디렉토리
|
|
49
|
+
* - externalMedia: 앱 전용 외부 미디어 디렉토리
|
|
50
|
+
* - appData: 앱 데이터 디렉토리
|
|
51
|
+
* - appFiles: 앱 파일 디렉토리
|
|
52
|
+
* - appCache: 앱 캐시 디렉토리
|
|
53
|
+
*/
|
|
54
|
+
static async getStoragePathAsync(type: TStorage): Promise<string> {
|
|
55
|
+
const result = await FileSystemPlugin.getStoragePath({ type });
|
|
56
|
+
return result.path;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 파일 URI 얻기 (FileProvider)
|
|
61
|
+
*/
|
|
62
|
+
static async getFileUriAsync(filePath: string): Promise<string> {
|
|
63
|
+
const result = await FileSystemPlugin.getFileUri({ path: filePath });
|
|
64
|
+
return result.uri;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 파일 쓰기
|
|
69
|
+
*/
|
|
70
|
+
static async writeFileAsync(filePath: string, data: string | Buffer): Promise<void> {
|
|
71
|
+
if (Buffer.isBuffer(data)) {
|
|
72
|
+
await FileSystemPlugin.writeFile({
|
|
73
|
+
path: filePath,
|
|
74
|
+
data: data.toString("base64"),
|
|
75
|
+
encoding: "base64",
|
|
76
|
+
});
|
|
77
|
+
} else {
|
|
78
|
+
await FileSystemPlugin.writeFile({
|
|
79
|
+
path: filePath,
|
|
80
|
+
data,
|
|
81
|
+
encoding: "utf8",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 파일 읽기 (UTF-8 문자열)
|
|
88
|
+
*/
|
|
89
|
+
static async readFileStringAsync(filePath: string): Promise<string> {
|
|
90
|
+
const result = await FileSystemPlugin.readFile({ path: filePath, encoding: "utf8" });
|
|
91
|
+
return result.data;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 파일 읽기 (Buffer)
|
|
96
|
+
*/
|
|
97
|
+
static async readFileBufferAsync(filePath: string): Promise<Buffer> {
|
|
98
|
+
const result = await FileSystemPlugin.readFile({ path: filePath, encoding: "base64" });
|
|
99
|
+
return Buffer.from(result.data, "base64");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 파일/디렉토리 삭제 (재귀)
|
|
104
|
+
*/
|
|
105
|
+
static async removeAsync(targetPath: string): Promise<void> {
|
|
106
|
+
await FileSystemPlugin.remove({ path: targetPath });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 디렉토리 생성 (재귀)
|
|
111
|
+
*/
|
|
112
|
+
static async mkdirsAsync(targetPath: string): Promise<void> {
|
|
113
|
+
await FileSystemPlugin.mkdir({ path: targetPath });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 존재 여부 확인
|
|
118
|
+
*/
|
|
119
|
+
static async existsAsync(targetPath: string): Promise<boolean> {
|
|
120
|
+
const result = await FileSystemPlugin.exists({ path: targetPath });
|
|
121
|
+
return result.exists;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -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
|
+
checkPermission(): 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,107 @@
|
|
|
1
|
+
import { WebPlugin } from "@capacitor/core";
|
|
2
|
+
import { IFileInfo, IFileSystemPlugin, TStorage } from "../IFileSystemPlugin";
|
|
3
|
+
import { VirtualFileSystem } from "./VirtualFileSystem";
|
|
4
|
+
import path from "path";
|
|
5
|
+
|
|
6
|
+
export class FileSystemWeb extends WebPlugin implements IFileSystemPlugin {
|
|
7
|
+
private readonly _fs = new VirtualFileSystem("capacitor_web_virtual_fs");
|
|
8
|
+
|
|
9
|
+
async checkPermission(): Promise<{ granted: boolean }> {
|
|
10
|
+
return await Promise.resolve({ granted: true });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async requestPermission(): Promise<void> {}
|
|
14
|
+
|
|
15
|
+
async readdir(options: { path: string }): Promise<{ files: IFileInfo[] }> {
|
|
16
|
+
const entry = await this._fs.getEntry(options.path);
|
|
17
|
+
if (!entry || entry.kind !== "dir") {
|
|
18
|
+
throw new Error("Directory does not exist");
|
|
19
|
+
}
|
|
20
|
+
const files = await this._fs.listChildren(options.path);
|
|
21
|
+
return { files };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async getStoragePath(options: { type: TStorage }): Promise<{ path: string }> {
|
|
25
|
+
const base = "/webfs";
|
|
26
|
+
let storagePath: string;
|
|
27
|
+
switch (options.type) {
|
|
28
|
+
case "external":
|
|
29
|
+
storagePath = base + "/external";
|
|
30
|
+
break;
|
|
31
|
+
case "externalFiles":
|
|
32
|
+
storagePath = base + "/externalFiles";
|
|
33
|
+
break;
|
|
34
|
+
case "externalCache":
|
|
35
|
+
storagePath = base + "/externalCache";
|
|
36
|
+
break;
|
|
37
|
+
case "externalMedia":
|
|
38
|
+
storagePath = base + "/externalMedia";
|
|
39
|
+
break;
|
|
40
|
+
case "appData":
|
|
41
|
+
storagePath = base + "/appData";
|
|
42
|
+
break;
|
|
43
|
+
case "appFiles":
|
|
44
|
+
storagePath = base + "/appFiles";
|
|
45
|
+
break;
|
|
46
|
+
case "appCache":
|
|
47
|
+
storagePath = base + "/appCache";
|
|
48
|
+
break;
|
|
49
|
+
default:
|
|
50
|
+
throw new Error("Unknown storage type: " + options.type);
|
|
51
|
+
}
|
|
52
|
+
await this._fs.ensureDir(storagePath);
|
|
53
|
+
return { path: storagePath };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async getFileUri(options: { path: string }): Promise<{ uri: string }> {
|
|
57
|
+
const entry = await this._fs.getEntry(options.path);
|
|
58
|
+
if (!entry || entry.kind !== "file" || entry.dataBase64 == null) {
|
|
59
|
+
throw new Error("File not found: " + options.path);
|
|
60
|
+
}
|
|
61
|
+
const byteCharacters = atob(entry.dataBase64);
|
|
62
|
+
const byteNumbers = new Array(byteCharacters.length);
|
|
63
|
+
for (let i = 0; i < byteCharacters.length; i++) {
|
|
64
|
+
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
|
65
|
+
}
|
|
66
|
+
const blob = new Blob([new Uint8Array(byteNumbers)]);
|
|
67
|
+
return { uri: URL.createObjectURL(blob) };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async writeFile(options: {
|
|
71
|
+
path: string;
|
|
72
|
+
data: string;
|
|
73
|
+
encoding?: "utf8" | "base64";
|
|
74
|
+
}): Promise<void> {
|
|
75
|
+
await this._fs.ensureDir(path.dirname(options.path));
|
|
76
|
+
const dataBase64 = options.encoding === "base64" ? options.data : btoa(options.data);
|
|
77
|
+
await this._fs.putEntry({ path: options.path, kind: "file", dataBase64 });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async readFile(options: {
|
|
81
|
+
path: string;
|
|
82
|
+
encoding?: "utf8" | "base64";
|
|
83
|
+
}): Promise<{ data: string }> {
|
|
84
|
+
const entry = await this._fs.getEntry(options.path);
|
|
85
|
+
if (!entry || entry.kind !== "file" || entry.dataBase64 == null) {
|
|
86
|
+
throw new Error("File not found: " + options.path);
|
|
87
|
+
}
|
|
88
|
+
const data = options.encoding === "base64" ? entry.dataBase64 : atob(entry.dataBase64);
|
|
89
|
+
return { data };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async remove(options: { path: string }): Promise<void> {
|
|
93
|
+
const ok = await this._fs.deleteByPrefix(options.path);
|
|
94
|
+
if (!ok) {
|
|
95
|
+
throw new Error("Deletion failed");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async mkdir(options: { path: string }): Promise<void> {
|
|
100
|
+
await this._fs.ensureDir(options.path);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async exists(options: { path: string }): Promise<{ exists: boolean }> {
|
|
104
|
+
const entry = await this._fs.getEntry(options.path);
|
|
105
|
+
return { exists: !!entry };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { IFileInfo } from "../IFileSystemPlugin";
|
|
2
|
+
|
|
3
|
+
interface FsEntry {
|
|
4
|
+
path: string;
|
|
5
|
+
kind: "file" | "dir";
|
|
6
|
+
dataBase64?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class VirtualFileSystem {
|
|
10
|
+
private readonly _STORE_NAME = "entries";
|
|
11
|
+
private readonly _DB_VERSION = 1;
|
|
12
|
+
|
|
13
|
+
constructor(private readonly _dbName: string) {}
|
|
14
|
+
|
|
15
|
+
private async _openDb(): Promise<IDBDatabase> {
|
|
16
|
+
return await new Promise((resolve, reject) => {
|
|
17
|
+
const req = indexedDB.open(this._dbName, this._DB_VERSION);
|
|
18
|
+
req.onupgradeneeded = () => {
|
|
19
|
+
const db = req.result;
|
|
20
|
+
if (!db.objectStoreNames.contains(this._STORE_NAME)) {
|
|
21
|
+
db.createObjectStore(this._STORE_NAME, { keyPath: "path" });
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
req.onsuccess = () => resolve(req.result);
|
|
25
|
+
req.onerror = () => reject(req.error);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private async _withStore<T>(
|
|
30
|
+
mode: IDBTransactionMode,
|
|
31
|
+
fn: (store: IDBObjectStore) => Promise<T>,
|
|
32
|
+
): Promise<T> {
|
|
33
|
+
const db = await this._openDb();
|
|
34
|
+
return await new Promise((resolve, reject) => {
|
|
35
|
+
const tx = db.transaction(this._STORE_NAME, mode);
|
|
36
|
+
const store = tx.objectStore(this._STORE_NAME);
|
|
37
|
+
let result: T;
|
|
38
|
+
Promise.resolve(fn(store))
|
|
39
|
+
.then((r) => {
|
|
40
|
+
result = r;
|
|
41
|
+
})
|
|
42
|
+
.catch(reject);
|
|
43
|
+
tx.oncomplete = () => resolve(result);
|
|
44
|
+
tx.onerror = () => reject(tx.error);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async getEntry(filePath: string): Promise<FsEntry | undefined> {
|
|
49
|
+
return await this._withStore("readonly", async (store) => {
|
|
50
|
+
return await new Promise((resolve, reject) => {
|
|
51
|
+
const req = store.get(filePath);
|
|
52
|
+
req.onsuccess = () => resolve(req.result);
|
|
53
|
+
req.onerror = () => reject(req.error);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async putEntry(entry: FsEntry): Promise<void> {
|
|
59
|
+
return await this._withStore("readwrite", async (store) => {
|
|
60
|
+
return await new Promise((resolve, reject) => {
|
|
61
|
+
const req = store.put(entry);
|
|
62
|
+
req.onsuccess = () => resolve();
|
|
63
|
+
req.onerror = () => reject(req.error);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async deleteByPrefix(pathPrefix: string): Promise<boolean> {
|
|
69
|
+
return await this._withStore("readwrite", async (store) => {
|
|
70
|
+
return await new Promise((resolve, reject) => {
|
|
71
|
+
const req = store.openCursor();
|
|
72
|
+
let found = false;
|
|
73
|
+
req.onsuccess = () => {
|
|
74
|
+
const cursor = req.result;
|
|
75
|
+
if (!cursor) {
|
|
76
|
+
resolve(found);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const key = String(cursor.key);
|
|
80
|
+
if (key === pathPrefix || key.startsWith(pathPrefix + "/")) {
|
|
81
|
+
found = true;
|
|
82
|
+
cursor.delete();
|
|
83
|
+
}
|
|
84
|
+
cursor.continue();
|
|
85
|
+
};
|
|
86
|
+
req.onerror = () => reject(req.error);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async listChildren(dirPath: string): Promise<IFileInfo[]> {
|
|
92
|
+
const prefix = dirPath === "/" ? "/" : dirPath + "/";
|
|
93
|
+
return await this._withStore("readonly", async (store) => {
|
|
94
|
+
return await new Promise((resolve, reject) => {
|
|
95
|
+
const req = store.openCursor();
|
|
96
|
+
const map = new Map<string, boolean>();
|
|
97
|
+
req.onsuccess = () => {
|
|
98
|
+
const cursor = req.result;
|
|
99
|
+
if (!cursor) {
|
|
100
|
+
resolve(
|
|
101
|
+
Array.from(map.entries()).map(([name, isDirectory]) => ({ name, isDirectory })),
|
|
102
|
+
);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const key = String(cursor.key);
|
|
106
|
+
if (key.startsWith(prefix)) {
|
|
107
|
+
const rest = key.slice(prefix.length);
|
|
108
|
+
if (rest) {
|
|
109
|
+
const firstSeg = rest.split("/")[0];
|
|
110
|
+
if (firstSeg && !map.has(firstSeg)) {
|
|
111
|
+
const value = cursor.value as FsEntry;
|
|
112
|
+
map.set(firstSeg, value.kind === "dir");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
cursor.continue();
|
|
117
|
+
};
|
|
118
|
+
req.onerror = () => reject(req.error);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async ensureDir(dirPath: string): Promise<void> {
|
|
124
|
+
if (dirPath === "/") {
|
|
125
|
+
await this.putEntry({ path: "/", kind: "dir" });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const segments = dirPath.split("/").filter(Boolean);
|
|
129
|
+
let acc = "";
|
|
130
|
+
for (const seg of segments) {
|
|
131
|
+
acc += "/" + seg;
|
|
132
|
+
const existing = await this.getEntry(acc);
|
|
133
|
+
if (!existing) {
|
|
134
|
+
await this.putEntry({ path: acc, kind: "dir" });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|