Survivalcraft API 1.8.2.3 v1.8.2.3
Survivalcraft 2.4
载入中...
搜索中...
未找到
Storage.cs
浏览该文件的文档.
1#if ANDROID
2#pragma warning disable CA1416
3using Environment = Android.OS.Environment;
4using Android.OS;
5#elif IOS
6using Foundation;
7#else
8#if WINDOWS
9using System.Diagnostics;
10#endif
11#if BROWSER
12using System.Runtime.InteropServices;
13using Engine.Browser;
14using System.Runtime.InteropServices.JavaScript;
15#pragma warning disable CA1416
16#else
17using NativeFileDialogCore;
18#endif
19using System.Reflection;
20#endif // !ANDROID
21using System.Text;
22
23namespace Engine {
24 public static class Storage {
25#if !ANDROID
26 const bool m_isAndroidPlatform = false;
28 static object m_dataDirectoryCreationLock = new();
29#else
30 const bool m_isAndroidPlatform = true;
31#endif
32
33 public static void Initialize() {
34#if BROWSER
35 MountOPFS("/__root__");
36#endif
37 }
38
39 public static long FreeSpace {
40 get {
41#if ANDROID
42 try {
43 StatFs statFs = new(Environment.DataDirectory?.Path);
44 long num = statFs.BlockSizeLong;
45 return statFs.AvailableBlocksLong * num;
46 }
47 catch (Exception) {
48 return long.MaxValue;
49 }
50#elif IOS
51 try {
52 var paths = NSSearchPath.GetDirectories(NSSearchPathDirectory.DocumentDirectory,
53 NSSearchPathDomain.User, true);
54
55 var attributes = NSFileManager.DefaultManager.GetFileSystemAttributes(paths[0]);
56
57 return (long)attributes.FreeSize;
58 }
59 catch (Exception) {
60 return long.MaxValue;
61 }
62#else
63 string fullPath = Path.GetFullPath(ProcessPath("data:", false, false));
64 if (fullPath.Length > 0) {
65 try {
66 return new DriveInfo(fullPath.Substring(0, 1)).AvailableFreeSpace;
67 }
68 catch {
69 // ignored
70 }
71 }
72 return long.MaxValue;
73#endif
74 }
75 }
76
77 public static bool FileExists(string path) {
78#if ANDROID
79 string path2 = ProcessPath(path, false, false, out bool isApp);
80 if (isApp) {
81 return EngineActivity.m_activity.ApplicationContext?.Assets?.List(GetDirectoryName(path2))?.Contains(GetFileName(path2)) ?? false;
82 }
83#endif
84 return File.Exists(ProcessPath(path, false, m_isAndroidPlatform));
85 }
86
87 public static bool DirectoryExists(string path) => Directory.Exists(ProcessPath(path, false, m_isAndroidPlatform));
88
89 public static long GetFileSize(string path) => new FileInfo(ProcessPath(path, false, m_isAndroidPlatform)).Length;
90
91 public static DateTime GetFileLastWriteTime(string path) => File.GetLastWriteTimeUtc(ProcessPath(path, false, m_isAndroidPlatform));
92
93 public static Stream OpenFile(string path, OpenFileMode openFileMode) {
94 if (openFileMode != 0
95 && openFileMode != OpenFileMode.ReadWrite
96 && openFileMode != OpenFileMode.Create
97 && openFileMode != OpenFileMode.CreateOrOpen) {
98 throw new ArgumentException("openFileMode");
99 }
100#if ANDROID
101 string path2 = ProcessPath(path, openFileMode != OpenFileMode.Read, false, out bool isApp);
102 if (isApp) {
103 return EngineActivity.m_activity.ApplicationContext?.Assets?.Open(path2);
104 }
105#else
106 string path2 = ProcessPath(path, openFileMode != OpenFileMode.Read, false);
107#endif
108 FileMode mode;
109 switch (openFileMode) {
110 case OpenFileMode.Create: mode = FileMode.Create; break;
111 case OpenFileMode.CreateOrOpen: mode = FileMode.OpenOrCreate; break;
112 default: mode = FileMode.Open; break;
113 }
114 FileAccess access = openFileMode == OpenFileMode.Read ? FileAccess.Read : FileAccess.ReadWrite;
115 return File.Open(path2, mode, access, FileShare.Read);
116 }
117
118 public static void DeleteFile(string path) {
119 File.Delete(ProcessPath(path, true, m_isAndroidPlatform));
120 }
121
122 public static void CopyFile(string sourcePath, string destinationPath) {
123 using Stream stream = OpenFile(sourcePath, OpenFileMode.Read);
124 using Stream destination = OpenFile(destinationPath, OpenFileMode.Create);
125 stream.CopyTo(destination);
126 }
127
128 public static void MoveFile(string sourcePath, string destinationPath) {
129 string sourceFileName = ProcessPath(sourcePath, true, m_isAndroidPlatform);
130 string text = ProcessPath(destinationPath, true, m_isAndroidPlatform);
131 File.Delete(text);
132 File.Move(sourceFileName, text);
133 }
134
135 public static void CreateDirectory(string path) {
136 Directory.CreateDirectory(ProcessPath(path, true, m_isAndroidPlatform));
137 }
138
139 public static void DeleteDirectory(string path) {
140 Directory.Delete(ProcessPath(path, true, m_isAndroidPlatform));
141 }
142
143 public static void DeleteDirectory(string path, bool recursive) {
144 Directory.Delete(ProcessPath(path, true, m_isAndroidPlatform), recursive);
145 }
146
147 public static IEnumerable<string> ListFileNames(string path) =>
148 from s in Directory.EnumerateFiles(ProcessPath(path, false, m_isAndroidPlatform)) select Path.GetFileName(s);
149
150 public static IEnumerable<string> ListDirectoryNames(string path) {
151 return from s in Directory.EnumerateDirectories(ProcessPath(path, false, m_isAndroidPlatform))
152#if ANDROID
153 select Path.GetFileName(s)
154 into s
155 where s != ".__override__"
156 select s;
157#else
158 select Path.GetFileName(s);
159#endif
160 }
161
162 public static string ReadAllText(string path) => ReadAllText(path, Encoding.UTF8);
163
164 public static string ReadAllText(string path, Encoding encoding) {
165 using StreamReader streamReader = new(OpenFile(path, OpenFileMode.Read), encoding);
166 return streamReader.ReadToEnd();
167 }
168
169 public static void WriteAllText(string path, string text) {
170 WriteAllText(path, text, Encoding.UTF8);
171 }
172
173 public static void WriteAllText(string path, string text, Encoding encoding) {
174 using StreamWriter streamWriter = new(OpenFile(path, OpenFileMode.Create), encoding);
175 streamWriter.Write(text);
176 }
177
178 public static byte[] ReadAllBytes(string path) {
179 using BinaryReader binaryReader = new(OpenFile(path, OpenFileMode.Read));
180 return binaryReader.ReadBytes((int)binaryReader.BaseStream.Length);
181 }
182
183 public static void WriteAllBytes(string path, byte[] bytes) {
184 using BinaryWriter binaryWriter = new(OpenFile(path, OpenFileMode.Create));
185 binaryWriter.Write(bytes);
186 }
187
188 public static string GetSystemPath(string path) => ProcessPath(path, false, m_isAndroidPlatform);
189
190 public static string GetExtension(string path) {
191 int lastIndexOfPoint = path.LastIndexOf('.');
192 int lastIndexOfSlash = Math.Max(path.LastIndexOf('/'), path.LastIndexOf('\\'));
193 return lastIndexOfPoint >= 0 && (lastIndexOfSlash == -1 || lastIndexOfSlash < lastIndexOfPoint)
194 ? path.Substring(lastIndexOfPoint)
195 : string.Empty;
196 }
197
198 public static string GetFileName(string path) {
199 int num = Math.Max(path.LastIndexOf('/'), path.LastIndexOf('\\'));
200 return num >= 0 ? path.Substring(num + 1) : path;
201 }
202
203 public static string GetFileNameWithoutExtension(string path) {
204 string fileName = GetFileName(path);
205 int num = fileName.LastIndexOf('.');
206 return num >= 0 ? fileName.Substring(0, num) : fileName;
207 }
208
209 public static string GetDirectoryName(string path) {
210 int num = path.LastIndexOf('/');
211 return num >= 0 ? path.Substring(0, num).TrimEnd('/') : string.Empty;
212 }
213
214 public static string CombinePaths(params string[] paths) {
215 StringBuilder stringBuilder = new();
216 for (int i = 0; i < paths.Length; i++) {
217 if (paths[i].Length > 0) {
218 stringBuilder.Append(paths[i]);
219 if (i < paths.Length - 1
220 && (stringBuilder.Length == 0 || stringBuilder[^1] != '/')) {
221 stringBuilder.Append('/');
222 }
223 }
224 }
225 return stringBuilder.ToString();
226 }
227
228 public static string ChangeExtension(string path, string extension) =>
230
231#if ANDROID
232 public static string ProcessPath(string path, bool writeAccess, bool failIfApp) => ProcessPath(path, writeAccess, failIfApp, out _);
233 public static string ProcessPath(string path, bool writeAccess, bool failIfApp, out bool isApp) {
234 ArgumentNullException.ThrowIfNull(path);
235 if (Path.DirectorySeparatorChar != '/') {
236 path = path.Replace('/', Path.DirectorySeparatorChar);
237 }
238 if (Path.DirectorySeparatorChar != '\\') {
239 path = path.Replace('\\', Path.DirectorySeparatorChar);
240 }
241 if (path.StartsWith("app:")) {
242 if (failIfApp) {
243 throw new InvalidOperationException($"Access denied to \"{path}\".");
244 }
245 isApp = true;
246 return path.Substring(4).TrimStart(Path.DirectorySeparatorChar);
247 }
248 if (path.StartsWith("data:")) {
249 isApp = false;
250 return Path.Combine(
251 System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments),
252 path.Substring(5).TrimStart(Path.DirectorySeparatorChar)
253 );
254 }
255 if (path.StartsWith("android:")) {
256 isApp = false;
257 return Path.Combine(
258 CombinePaths(Environment.ExternalStorageDirectory?.AbsolutePath, path.Substring(8).TrimStart(Path.DirectorySeparatorChar))
259 );
260 }
261 if (path.StartsWith("config:")) {
262 isApp = false;
263 return Path.Combine(EngineActivity.ConfigPath, path.Substring(8).TrimStart(Path.DirectorySeparatorChar));
264 }
265 throw new InvalidOperationException($"Invalid path \"{path}\".");
266 }
267#elif IOS
268 public static string ProcessPath(string path, bool writeAccess, bool failIfApp) => ProcessPath(path, writeAccess, failIfApp, out _);
269 public static string ProcessPath(string path, bool writeAccess, bool failIfApp, out bool isApp) {
270 ArgumentNullException.ThrowIfNull(path);
271 if (Path.DirectorySeparatorChar != '/') {
272 path = path.Replace('/', Path.DirectorySeparatorChar);
273 }
274 if (Path.DirectorySeparatorChar != '\\') {
275 path = path.Replace('\\', Path.DirectorySeparatorChar);
276 }
277 if (path.StartsWith("app:") || path.StartsWith("data:")) {
278 isApp = false;
279 return Path.Combine(
280 System.Environment.GetFolderPath(System.Environment.SpecialFolder.MyDocuments),
281 path.Substring(5).TrimStart(Path.DirectorySeparatorChar)
282 );
283 }
284 throw new InvalidOperationException($"Invalid path \"{path}\".");
285 }
286#else
287#if BROWSER
288 public static string GetAppDirectory(bool failIfApp) => Path.DirectorySeparatorChar.ToString();
289#else
290 public static string GetAppDirectory(bool failIfApp) => failIfApp
291 ? throw new InvalidOperationException("Access denied.")
292#pragma warning disable IL3000
293 : Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location);
294#pragma warning restore IL3000
295#endif
296 public static string GetDataDirectory(bool writeAccess) {
297 string text = Path.Combine(
298 Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
299 Assembly.GetEntryAssembly()!.GetName()!.Name!
300 );
301 if (writeAccess) {
304 return text;
305 }
306 Directory.CreateDirectory(text);
308 return text;
309 }
310 }
311 return text;
312 }
313
314 public static string ProcessPath(string path, bool writeAccess, bool failIfApp) {
315 ArgumentNullException.ThrowIfNull(path);
316 if (Path.DirectorySeparatorChar != '/') {
317 path = path.Replace('/', Path.DirectorySeparatorChar);
318 }
319 if (Path.DirectorySeparatorChar != '\\') {
320 path = path.Replace('\\', Path.DirectorySeparatorChar);
321 }
322 string text;
323 if (path.StartsWith("app:")) {
324 text = GetAppDirectory(failIfApp);
325 path = path.Substring(4).TrimStart(Path.DirectorySeparatorChar);
326 }
327 else if (path.StartsWith("data:")) {
328 text = GetDataDirectory(writeAccess);
329 path = path.Substring(5).TrimStart(Path.DirectorySeparatorChar);
330 }
331 else {
332 if (!path.StartsWith("system:")) {
333#if BROWSER
334 EnsurePathLinked(path);
335 return path;
336#else
337 throw new InvalidOperationException("Invalid path.");
338#endif
339 }
340 text = string.Empty;
341 path = path.Substring(7);
342 }
343 string result = string.IsNullOrEmpty(text) ? path : Path.Combine(text, path);
344#if BROWSER
345 EnsurePathLinked(result);
346#endif
347 return result;
348 }
349#endif
350 public static void MoveDirectory(string path, string newPath) => Directory.Move(ProcessPath(path, true, false), ProcessPath(newPath, true, false));
351
352 public static void DeleteDirectoryRecursive(string path) => Directory.Delete(ProcessPath(path, true, false));
353
354 public static DirectoryInfo GetDirectoryInfo(string path) => new(ProcessPath(path, true, false));
355
356 public static FileInfo GetFileInfo(string path) => new(ProcessPath(path, true, false));
357
358 public static char[] InvalidFileNameChars = [
359 '\\',
360 '/',
361 ':',
362 '*',
363 '?',
364 '"',
365 '<',
366 '>',
367 '|',
368 '\0'
369 ];
370
371 public static string SanitizeFileName(string filename, string replacement = "-") {
372 StringBuilder sanitized = new();
373 foreach (char c in filename) {
374 sanitized.Append(InvalidFileNameChars.Contains(c) ? replacement : c);
375 }
376 return sanitized.ToString();
377 }
378
379 /*
380 * <Summary>
381 * 使用外部应用打开文件
382 * </Summary>
383 * <Param name="path">文件路径</Param>
384 * <Param name="chooserTitle">(仅安卓)应用选择器标题,留空时使用文件名</Param>
385 * <Param name="mimeType">(仅安卓)MIME 类型,留空时自动根据文件后缀推断</Param>
386 */
387 public static void OpenFileWithExternalApplication(string path, string chooserTitle = null, string mimeType = null) {
388 if (!FileExists(path)) {
389 throw new FileNotFoundException($"Open {path} failed, because it is not exists.");
390 }
391 path = ProcessPath(path, false, false);
392#if WINDOWS
393 Process.Start("explorer.exe", path);
394#elif LINUX
395 Process.Start("xdg-open", path);
396#elif ANDROID
397 Window.Activity.OpenFile(path, chooserTitle, mimeType);
398#endif
399 }
400
401 /*
402 * <Summary>
403 * 分享文件,当前版本仅支持安卓、浏览器,浏览器上的形式为下载
404 * </Summary>
405 * <Param name="path">文件路径</Param>
406 * <Param name="chooserTitle">(浏览器无效)应用选择器标题,留空时使用文件名</Param>
407 * <Param name="mimeType">MIME 类型,留空时自动根据文件后缀推断</Param>
408 */
409 public static async Task ShareFile(string path, string chooserTitle = null, string mimeType = null) {
410 if (!FileExists(path)) {
411 throw new FileNotFoundException($"Share {path} failed, because it is not exists.");
412 }
413#if ANDROID
414 path = ProcessPath(path, false, false);
415 Window.Activity.ShareFile(path, chooserTitle, mimeType);
416#elif BROWSER
417 JSObject fileHandle = await BrowserInterop.ShowSaveFilePicker(Storage.GetFileName(path), mimeType);
418 Stream stream = OpenFile(path, OpenFileMode.Read);
419 byte[] bytes = new byte[stream.Length];
420 _ = await stream.ReadAsync(bytes);
421 await BrowserInterop.SaveBytesToFileHandle(fileHandle, bytes);
422#endif
423 }
424
425 /*
426 * <Summary>
427 * 使用系统文件选择器选择并打开文件
428 * </Summary>
429 * <Param name="title">文件选择器的标题</Param>
430 * <Param name="filters">(安卓无效)过滤器列表。键为名称,例如“图片”;值为通配符列表,例如["*.png", "*.jpg"]</Param>
431 * <Param name="defaultPath">(安卓无效)默认路径。其中浏览器只支持"desktop"、"documents"、"downloads"、"music"、"pictures" 或 "videos"</Param>
432 * <Param name="mode">(安卓、浏览器只读)文件打开模式</Param>
433 */
434#pragma warning disable CS1998
435 public static async Task<(Stream, string)> ChooseFile(string title = null,
436 KeyValuePair<string, string[]>[] filters = null,
437 string defaultPath = null,
438 OpenFileMode mode = OpenFileMode.Read) {
439#pragma warning restore CS1998
440 if (mode == OpenFileMode.Create
441 || mode == OpenFileMode.CreateOrOpen) {
442 throw new ArgumentException("mode");
443 }
444#if ANDROID
445 return await Window.Activity.ChooseFileAsync(title);
446#elif IOS
447 throw new Exception("Unsupported Operation");
448#elif BROWSER
449 List<string> descAndExtArray = [];
450 List<int> extCounts = [];
451 if (filters != null) {
452 foreach (KeyValuePair<string, string[]> filter in filters) {
453 descAndExtArray.Add(filter.Key);
454 extCounts.Add(filter.Value.Length);
455 string[] extensions = filter.Value;
456 foreach (string extension in extensions) {
457 descAndExtArray.Add(extension);
458 }
459 }
460 }
461 JSObject file = null;
462 try {
463 file = await BrowserInterop.ShowOpenFilePicker(descAndExtArray.ToArray(), extCounts.ToArray(), defaultPath);
464 }
465 catch {
466 // ignore
467 }
468 if (file == null) {
469 return (null, null);
470 }
471 string fileName = BrowserInterop.GetFileName(file);
472 if (string.IsNullOrEmpty(fileName)) {
473 return (null, null);
474 }
475 JSObject bytes = await BrowserInterop.GetFileBytes(file);
476 file.Dispose();
477 Stream stream = new MemoryStream(BrowserInterop.JSObject2ByteArray(bytes));
478 bytes.Dispose();
479 stream.Position = 0;
480 return (stream, fileName);
481#else
482 string filtersString = null;
483 if (filters != null) {
484 StringBuilder sb = new();
485 bool firstAdded1 = false;
486 foreach (KeyValuePair<string, string[]> filter in filters) {
487 if (firstAdded1) {
488 sb.Append(';');
489 }
490 else {
491 firstAdded1 = true;
492 }
493 sb.Append('[');
494 sb.Append(filter.Key);
495 sb.Append('|');
496 bool firstAdded2 = false;
497 foreach (string ext in filter.Value) {
498 if (firstAdded2) {
499 sb.Append(',');
500 }
501 else {
502 firstAdded2 = true;
503 }
504 sb.Append(ext);
505 }
506 sb.Append(']');
507 }
508 filtersString = sb.ToString();
509 }
510 DialogResult result = Dialog.FileOpenEx(
511 filtersString,
512 defaultPath,
513 title,
514 null,
515 null,
516 null,
518 );
519 if (result.IsOk
520 && !string.IsNullOrEmpty(result.Path)) {
521 try {
522 Stream stream = File.Open(
523 result.Path,
524 FileMode.Open,
525 mode == OpenFileMode.Read ? FileAccess.Read : FileAccess.ReadWrite,
526 FileShare.Read
527 );
528 return (stream, GetFileName(result.Path));
529 }
530 catch (Exception e) {
531 Log.Error($"Choose file failed. File path: \"{result.Path}\". Reason: {e.Message}");
532 return (null, result.Path);
533 }
534 }
535 return (null, null);
536#endif
537 }
538
539#if BROWSER
540 static HashSet<string> m_linkedPaths = ["__root__", "dev", "tmp"];
544 static void EnsurePathLinked(string path) {
545 if (string.IsNullOrEmpty(path)) {
546 return;
547 }
548 path = path.Replace('\\', '/');
549 string[] parts = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
550 if (parts.Length == 0) {
551 return;
552 }
553 string topDir = parts[0];
554 // 如果已经是系统目录或挂载根目录,直接跳过
555 if (m_linkedPaths.Contains(topDir)) {
556 return;
557 }
558 string linkPath = $"/{topDir}";
559 string realPath = $"/__root__/{topDir}";
560 if (!Directory.Exists(realPath)) {
561 Directory.CreateDirectory(realPath);
562 }
563 int res = CreateSymlink(realPath, linkPath);
564 switch (res) {
565 case 0:
566 m_linkedPaths.Add(topDir);
567 break;
568 case -17:// EEXIST
569 m_linkedPaths.Add(topDir);
570 // 可以在这里加个校验,确定它是不是指向正确的地方,但通常没必要
571 break;
572 default: throw new Exception($"Failed to link \"{linkPath}\" to \"{realPath}\", error code: {res}");
573 }
574 }
575
576 [DllImport("wasmfsHelper", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
577 [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
578 static extern int MountOPFS(string mountPath);
579
580 [DllImport("wasmfsHelper", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.Cdecl)]
581 [DefaultDllImportSearchPaths(DllImportSearchPath.SafeDirectories)]
582 static extern int CreateSymlink(string target, string linkpath);
583#endif
584 }
585}
System.Environment Environment
static partial string GetFileName(JSObject file)
static partial Task< JSObject > ShowOpenFilePicker(string[] descAndExtArray, int[] extCounts, string defaultPath)
static partial Task SaveBytesToFileHandle(JSObject fileHandle, byte[] bytes)
static partial Task< JSObject > GetFileBytes(JSObject file)
static partial Task< JSObject > ShowSaveFilePicker(string fileName, string mimeType)
static partial byte[] JSObject2ByteArray(JSObject obj)
static void Error(object message)
定义 Log.cs:80
static void MoveFile(string sourcePath, string destinationPath)
static string GetSystemPath(string path)
static void CreateDirectory(string path)
static byte[] ReadAllBytes(string path)
static bool DirectoryExists(string path)
static void DeleteDirectory(string path, bool recursive)
static IEnumerable< string > ListDirectoryNames(string path)
static IEnumerable< string > ListFileNames(string path)
static string ChangeExtension(string path, string extension)
static Stream OpenFile(string path, OpenFileMode openFileMode)
static string GetExtension(string path)
const bool m_isAndroidPlatform
static char[] InvalidFileNameChars
static string SanitizeFileName(string filename, string replacement="-")
static FileInfo GetFileInfo(string path)
static void OpenFileWithExternalApplication(string path, string chooserTitle=null, string mimeType=null)
static DateTime GetFileLastWriteTime(string path)
static void WriteAllText(string path, string text, Encoding encoding)
static string ProcessPath(string path, bool writeAccess, bool failIfApp)
static bool FileExists(string path)
static string GetDirectoryName(string path)
static string ReadAllText(string path)
static string GetAppDirectory(bool failIfApp)
static void DeleteDirectoryRecursive(string path)
static string GetFileNameWithoutExtension(string path)
static string GetDataDirectory(bool writeAccess)
static object m_dataDirectoryCreationLock
static void MoveDirectory(string path, string newPath)
static string GetFileName(string path)
static string ReadAllText(string path, Encoding encoding)
static bool m_dataDirectoryCreated
static async Task<(Stream, string)> ChooseFile(string title=null, KeyValuePair< string, string[]>[] filters=null, string defaultPath=null, OpenFileMode mode=OpenFileMode.Read)
static long FreeSpace
static long GetFileSize(string path)
static void WriteAllBytes(string path, byte[] bytes)
static void DeleteDirectory(string path)
static void DeleteFile(string path)
static string CombinePaths(params string[] paths)
static void CopyFile(string sourcePath, string destinationPath)
static DirectoryInfo GetDirectoryInfo(string path)
static void WriteAllText(string path, string text)
static void Initialize()
static async Task ShareFile(string path, string chooserTitle=null, string mimeType=null)
static IntPtr Handle
int MountOPFS(const char *mountPath)
int CreateSymlink(const char *target, const char *linkpath)