expo-tiddlywiki-filesystem-android-external-storage 2.2.12 → 2.2.14
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/java/expo/modules/externalstorage/ExternalStorageModule.kt +8 -0
- package/android/src/main/java/expo/modules/externalstorage/GitHelper.kt +139 -0
- package/build/index.d.ts +23 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -2
- package/build/index.js.map +1 -1
- package/expo-module.config.json +4 -1
- package/ios/ExternalStorageModule.swift +359 -0
- package/ios/GitHelper.swift +697 -0
- package/ios/TarExtractor.swift +158 -0
- package/ios/TiddlerParser.swift +254 -0
- package/package.json +3 -2
- package/src/index.ts +27 -2
package/android/build.gradle
CHANGED
|
@@ -12,6 +12,15 @@ android {
|
|
|
12
12
|
versionCode 1
|
|
13
13
|
versionName '0.1.0'
|
|
14
14
|
}
|
|
15
|
+
// JGit needs Java 11+
|
|
16
|
+
compileOptions {
|
|
17
|
+
sourceCompatibility JavaVersion.VERSION_11
|
|
18
|
+
targetCompatibility JavaVersion.VERSION_11
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
dependencies {
|
|
23
|
+
implementation 'org.eclipse.jgit:org.eclipse.jgit:6.10.0.202406032230-r'
|
|
15
24
|
}
|
|
16
25
|
|
|
17
26
|
dependencies {
|
|
@@ -564,6 +564,14 @@ class ExternalStorageModule : Module() {
|
|
|
564
564
|
GitHelper.buildGitIndex(gitRootDir)
|
|
565
565
|
}
|
|
566
566
|
|
|
567
|
+
AsyncFunction("gitPush") { gitRootDir: String, remoteName: String, localBranch: String, remoteBranch: String, force: Boolean, headers: String? ->
|
|
568
|
+
GitHelper.gitPush(gitRootDir, remoteName, localBranch, remoteBranch, force, headers)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
AsyncFunction("gitFetch") { gitRootDir: String, remoteName: String, branch: String, headers: String? ->
|
|
572
|
+
GitHelper.gitFetch(gitRootDir, remoteName, branch, headers)
|
|
573
|
+
}
|
|
574
|
+
|
|
567
575
|
// ─── TiddlyWiki batch file parsing ─────────────────────────────────
|
|
568
576
|
|
|
569
577
|
/**
|
|
@@ -643,4 +643,143 @@ internal object GitHelper {
|
|
|
643
643
|
return ((bytes[offset].toInt() and 0xFF) shl 8) or
|
|
644
644
|
(bytes[offset + 1].toInt() and 0xFF)
|
|
645
645
|
}
|
|
646
|
+
|
|
647
|
+
// ─── JGit push/fetch ──────────────────────────────────────────
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Push local branch to remote using JGit (efficient native pack building).
|
|
651
|
+
* @param gitRootDir absolute path to the git working directory
|
|
652
|
+
* @param remoteName remote name (e.g. "origin")
|
|
653
|
+
* @param localBranch local branch name (e.g. "main")
|
|
654
|
+
* @param remoteBranch remote branch ref (e.g. "refs/heads/mobile-incoming")
|
|
655
|
+
* @param force whether to force push
|
|
656
|
+
* @param headers optional HTTP headers as JSON object string
|
|
657
|
+
* @return JSON string with result info
|
|
658
|
+
*/
|
|
659
|
+
fun gitPush(
|
|
660
|
+
gitRootDir: String,
|
|
661
|
+
remoteName: String,
|
|
662
|
+
localBranch: String,
|
|
663
|
+
remoteBranch: String,
|
|
664
|
+
force: Boolean,
|
|
665
|
+
headers: String?
|
|
666
|
+
): String {
|
|
667
|
+
val result = JSONObject()
|
|
668
|
+
try {
|
|
669
|
+
val root = File(gitRootDir)
|
|
670
|
+
val git = org.eclipse.jgit.api.Git.open(root)
|
|
671
|
+
try {
|
|
672
|
+
val pushCommand = git.push()
|
|
673
|
+
.setRemote(remoteName)
|
|
674
|
+
.setRefSpecs(org.eclipse.jgit.transport.RefSpec("refs/heads/$localBranch:$remoteBranch"))
|
|
675
|
+
.setForce(force)
|
|
676
|
+
|
|
677
|
+
// Set custom HTTP headers if provided
|
|
678
|
+
if (headers != null) {
|
|
679
|
+
try {
|
|
680
|
+
val headerObj = JSONObject(headers)
|
|
681
|
+
val headerMap = mutableMapOf<String, String>()
|
|
682
|
+
for (key in headerObj.keys()) {
|
|
683
|
+
headerMap[key] = headerObj.getString(key)
|
|
684
|
+
}
|
|
685
|
+
// JGit supports custom transport configuration
|
|
686
|
+
pushCommand.setTransportConfigCallback { transport ->
|
|
687
|
+
if (transport is org.eclipse.jgit.transport.http.HttpTransport) {
|
|
688
|
+
transport.additionalHeaders = headerMap
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
} catch (e: Exception) {
|
|
692
|
+
android.util.Log.w("GitPush", "Failed to parse headers: ${e.message}")
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
val pushResults = pushCommand.call()
|
|
697
|
+
|
|
698
|
+
val resultsArray = JSONArray()
|
|
699
|
+
for (pushResult in pushResults) {
|
|
700
|
+
for (update in pushResult.remoteUpdates) {
|
|
701
|
+
val updateObj = JSONObject()
|
|
702
|
+
updateObj.put("remoteName", update.remoteName)
|
|
703
|
+
updateObj.put("status", update.status.name)
|
|
704
|
+
updateObj.put("message", update.message ?: "")
|
|
705
|
+
resultsArray.put(updateObj)
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
result.put("ok", true)
|
|
710
|
+
result.put("updates", resultsArray)
|
|
711
|
+
} finally {
|
|
712
|
+
git.close()
|
|
713
|
+
}
|
|
714
|
+
} catch (e: Exception) {
|
|
715
|
+
result.put("ok", false)
|
|
716
|
+
result.put("error", e.message ?: "Unknown push error")
|
|
717
|
+
android.util.Log.e("GitPush", "Push failed: ${e.message}", e)
|
|
718
|
+
}
|
|
719
|
+
return result.toString()
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Fetch from remote using JGit (efficient native pack handling).
|
|
724
|
+
* @param gitRootDir absolute path to the git working directory
|
|
725
|
+
* @param remoteName remote name (e.g. "origin")
|
|
726
|
+
* @param branch branch to fetch
|
|
727
|
+
* @param headers optional HTTP headers as JSON object string
|
|
728
|
+
* @return JSON string with result info
|
|
729
|
+
*/
|
|
730
|
+
fun gitFetch(
|
|
731
|
+
gitRootDir: String,
|
|
732
|
+
remoteName: String,
|
|
733
|
+
branch: String,
|
|
734
|
+
headers: String?
|
|
735
|
+
): String {
|
|
736
|
+
val result = JSONObject()
|
|
737
|
+
try {
|
|
738
|
+
val root = File(gitRootDir)
|
|
739
|
+
val git = org.eclipse.jgit.api.Git.open(root)
|
|
740
|
+
try {
|
|
741
|
+
val fetchCommand = git.fetch()
|
|
742
|
+
.setRemote(remoteName)
|
|
743
|
+
.setRefSpecs(org.eclipse.jgit.transport.RefSpec("+refs/heads/$branch:refs/remotes/$remoteName/$branch"))
|
|
744
|
+
|
|
745
|
+
if (headers != null) {
|
|
746
|
+
try {
|
|
747
|
+
val headerObj = JSONObject(headers)
|
|
748
|
+
val headerMap = mutableMapOf<String, String>()
|
|
749
|
+
for (key in headerObj.keys()) {
|
|
750
|
+
headerMap[key] = headerObj.getString(key)
|
|
751
|
+
}
|
|
752
|
+
fetchCommand.setTransportConfigCallback { transport ->
|
|
753
|
+
if (transport is org.eclipse.jgit.transport.http.HttpTransport) {
|
|
754
|
+
transport.additionalHeaders = headerMap
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
} catch (e: Exception) {
|
|
758
|
+
android.util.Log.w("GitFetch", "Failed to parse headers: ${e.message}")
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
val fetchResult = fetchCommand.call()
|
|
763
|
+
|
|
764
|
+
val updatesArray = JSONArray()
|
|
765
|
+
for ((refName, update) in fetchResult.trackingRefUpdates.associate { it.localName to it }) {
|
|
766
|
+
val updateObj = JSONObject()
|
|
767
|
+
updateObj.put("ref", refName)
|
|
768
|
+
updateObj.put("oldObjectId", update.oldObjectId?.name ?: "")
|
|
769
|
+
updateObj.put("newObjectId", update.newObjectId?.name ?: "")
|
|
770
|
+
updatesArray.put(updateObj)
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
result.put("ok", true)
|
|
774
|
+
result.put("updates", updatesArray)
|
|
775
|
+
} finally {
|
|
776
|
+
git.close()
|
|
777
|
+
}
|
|
778
|
+
} catch (e: Exception) {
|
|
779
|
+
result.put("ok", false)
|
|
780
|
+
result.put("error", e.message ?: "Unknown fetch error")
|
|
781
|
+
android.util.Log.e("GitFetch", "Fetch failed: ${e.message}", e)
|
|
782
|
+
}
|
|
783
|
+
return result.toString()
|
|
784
|
+
}
|
|
646
785
|
}
|
package/build/index.d.ts
CHANGED
|
@@ -149,6 +149,29 @@ interface IExternalStorageModule {
|
|
|
149
149
|
* @returns JSON string: `{"ok":true,"entries":N,"indexSize":M}` or `{"ok":false,"error":"..."}`
|
|
150
150
|
*/
|
|
151
151
|
buildGitIndex(gitRootDir: string): Promise<string>;
|
|
152
|
+
/**
|
|
153
|
+
* Push local branch to remote using native JGit (efficient pack building).
|
|
154
|
+
* Avoids OOM from isomorphic-git's JS-based pack construction on large repos.
|
|
155
|
+
*
|
|
156
|
+
* @param gitRootDir Absolute path to the git working directory
|
|
157
|
+
* @param remoteName Remote name (e.g. "origin")
|
|
158
|
+
* @param localBranch Local branch name (e.g. "main")
|
|
159
|
+
* @param remoteBranch Remote branch ref (e.g. "refs/heads/mobile-incoming")
|
|
160
|
+
* @param force Whether to force push
|
|
161
|
+
* @param headers Optional HTTP headers as JSON string
|
|
162
|
+
* @returns JSON string: `{"ok":true,"updates":[...]}` or `{"ok":false,"error":"..."}`
|
|
163
|
+
*/
|
|
164
|
+
gitPush(gitRootDir: string, remoteName: string, localBranch: string, remoteBranch: string, force: boolean, headers?: string | null): Promise<string>;
|
|
165
|
+
/**
|
|
166
|
+
* Fetch from remote using native JGit (efficient pack handling).
|
|
167
|
+
*
|
|
168
|
+
* @param gitRootDir Absolute path to the git working directory
|
|
169
|
+
* @param remoteName Remote name (e.g. "origin")
|
|
170
|
+
* @param branch Branch to fetch
|
|
171
|
+
* @param headers Optional HTTP headers as JSON string
|
|
172
|
+
* @returns JSON string: `{"ok":true,"updates":[...]}` or `{"ok":false,"error":"..."}`
|
|
173
|
+
*/
|
|
174
|
+
gitFetch(gitRootDir: string, remoteName: string, branch: string, headers?: string | null): Promise<string>;
|
|
152
175
|
}
|
|
153
176
|
export declare const ExternalStorage: IExternalStorageModule;
|
|
154
177
|
/**
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/build/index.js
CHANGED
|
@@ -18,8 +18,8 @@ let _module;
|
|
|
18
18
|
function getNativeModule() {
|
|
19
19
|
if (_module)
|
|
20
20
|
return _module;
|
|
21
|
-
if (Platform.OS !== 'android') {
|
|
22
|
-
throw new Error('ExternalStorage native module is only available on Android');
|
|
21
|
+
if (Platform.OS !== 'android' && Platform.OS !== 'ios') {
|
|
22
|
+
throw new Error('ExternalStorage native module is only available on Android and iOS');
|
|
23
23
|
}
|
|
24
24
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
25
25
|
const { requireNativeModule } = require('expo-modules-core');
|
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAExC,IAAI,OAA2C,CAAC;AAEhD;;;;GAIG;AACH,SAAS,eAAe;IACtB,IAAI,OAAO;QAAE,OAAO,OAAO,CAAC;IAC5B,IAAI,QAAQ,CAAC,EAAE,KAAK,SAAS,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAC;IAChF,CAAC;IACD,iEAAiE;IACjE,MAAM,EAAE,mBAAmB,EAAE,GAAG,OAAO,CAAC,mBAAmB,CAAsE,CAAC;IAClI,OAAO,GAAG,mBAAmB,CAAC,iBAAiB,CAAC,CAAC;IACjD,OAAO,OAAO,CAAC;AACjB,CAAC;AAsLD,MAAM,CAAC,MAAM,eAAe,GAA2B,IAAI,KAAK,CAAC,EAA4B,EAAE;IAC7F,GAAG,CAAC,OAAO,EAAE,QAAQ;QACnB,MAAM,GAAG,GAAG,eAAe,EAAE,CAAC;QAC9B,OAAQ,GAAmD,CAAC,QAAQ,CAAC,CAAC;IACxE,CAAC;CACF,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,SAAiB;IAC3C,IAAI,SAAS,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACpC,OAAO,SAAS,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAC3C,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC","sourcesContent":["/**\n * TypeScript bindings for the ExternalStorage native module.\n *\n * This module uses raw java.io.File on Android to bypass Expo FileSystem's\n * directory whitelist. It allows reading/writing to shared external storage\n * when MANAGE_EXTERNAL_STORAGE permission is granted.\n *\n * All path arguments are plain filesystem paths (e.g. \"/storage/emulated/0/Documents/TidGi/\").\n * Do NOT pass file:// URIs — strip the scheme before calling.\n */\nimport { Platform } from 'react-native';\n\nlet _module: IExternalStorageModule | undefined;\n\n/**\n * Lazily load the native module. Wrapped in a function so that the app does NOT\n * crash at import time if the native module is missing (e.g. on iOS or when the\n * binary was built without it).\n */\nfunction getNativeModule(): IExternalStorageModule {\n if (_module) return _module;\n if (Platform.OS !== 'android') {\n throw new Error('ExternalStorage native module is only available on Android');\n }\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { requireNativeModule } = require('expo-modules-core') as { requireNativeModule: (name: string) => IExternalStorageModule };\n _module = requireNativeModule('ExternalStorage');\n return _module;\n}\n\ninterface FileInfo {\n exists: boolean;\n isDirectory: boolean;\n size: number;\n /** Milliseconds since epoch */\n modificationTime: number;\n}\n\ninterface BatchWriteResult {\n writtenCount: number;\n}\n\ninterface HttpPostToFileResult {\n statusCode: number;\n headers: Record<string, string>;\n bytesWritten: number;\n}\n\ninterface DownloadFileResumableResult {\n statusCode: number;\n /** Final size of the file on disk after download */\n totalBytes: number;\n /** true if the download resumed from a partial file (HTTP 206) */\n resumed: boolean;\n}\n\ninterface ExtractTarResult {\n filesExtracted: number;\n}\n\ninterface ReadFileChunkResult {\n /** Base64-encoded chunk data */\n data: string;\n bytesRead: number;\n}\n\ninterface IExternalStorageModule {\n exists(path: string): Promise<boolean>;\n getInfo(path: string): Promise<FileInfo>;\n\n mkdir(path: string): Promise<void>;\n readDir(path: string): Promise<string[]>;\n /** Recursively list all files under a directory, returning relative paths. Skips .git etc. */\n readDirRecursive(path: string): Promise<string[]>;\n rmdir(path: string): Promise<void>;\n\n readFileUtf8(path: string): Promise<string>;\n readFileBase64(path: string): Promise<string>;\n writeFileUtf8(path: string, content: string): Promise<void>;\n writeFileBase64(path: string, base64Content: string): Promise<void>;\n /**\n * Append a Base64-encoded chunk to a file, optionally truncating first.\n *\n * Designed for streaming large writes from JS in bounded-memory chunks\n * (e.g. 512 KB each) so the JVM never allocates the full file content,\n * avoiding OOM on 50+ MB git pack files.\n *\n * @param truncateFirst Pass `true` for the first chunk to create/truncate\n * the file, then `false` for subsequent chunks.\n */\n appendFileBase64(path: string, base64Content: string, truncateFirst: boolean): Promise<void>;\n writeFilesBase64(paths: string[], base64Contents: string[]): Promise<BatchWriteResult>;\n deleteFile(path: string): Promise<void>;\n\n isExternalStorageWritable(): Promise<boolean>;\n getExternalStorageDirectory(): Promise<string>;\n /** Android 11+ (API 30): check if MANAGE_EXTERNAL_STORAGE is granted. Pre-30 returns true. */\n isExternalStorageManager(): Promise<boolean>;\n\n /**\n * HTTP POST with the response body streamed directly to a file on disk,\n * **never buffering the full response in JVM/Hermes heap**.\n *\n * Designed for git-upload-pack which can return 100+ MB packfiles.\n *\n * @param url Target URL\n * @param headers HTTP headers as `{ key: value }`\n * @param bodyBase64 Request body encoded as Base64 (binary git protocol data)\n * @param destPath Plain filesystem path to write the response body to\n * @param contentType MIME type for the request body\n */\n httpPostToFile(\n url: string,\n headers: Record<string, string>,\n bodyBase64: string,\n destPath: string,\n contentType: string,\n ): Promise<HttpPostToFileResult>;\n\n /**\n * Read a chunk of a file starting at `offset` for up to `length` bytes.\n * Returns Base64-encoded data and actual bytes read.\n *\n * Use this to stream a large file into JS in bounded-memory chunks.\n */\n readFileChunk(path: string, offset: number, length: number): Promise<ReadFileChunkResult>;\n\n /**\n * Download a file via HTTP GET with resumable download support.\n *\n * If `destPath` already exists on disk (from a previous interrupted download),\n * sends `Range: bytes=<existingSize>-` to resume. The server must respond\n * with 206 Partial Content for resume to work; otherwise the file is\n * overwritten from scratch (200 response).\n *\n * @param url Target URL\n * @param headers Extra HTTP headers (e.g. Authorization, ETag)\n * @param destPath Plain filesystem path for the downloaded file\n */\n downloadFileResumable(\n url: string,\n headers: Record<string, string>,\n destPath: string,\n ): Promise<DownloadFileResumableResult>;\n\n /**\n * Extract an uncompressed tar archive to a destination directory.\n * Uses a native tar parser — no third-party dependency.\n * Supports POSIX ustar and GNU long-name extensions.\n * Validates paths to prevent directory traversal attacks.\n *\n * @param tarPath Path to the .tar file\n * @param destDir Destination directory (created if needed)\n */\n extractTar(tarPath: string, destDir: string): Promise<ExtractTarResult>;\n\n /**\n * Parse a batch of TiddlyWiki tiddler files entirely in native Kotlin.\n *\n * This is the critical performance optimization for initial wiki loading:\n * a single bridge call processes 100+ files in parallel, returning a\n * ready-to-inject JSON array string. Eliminates per-file bridge round-trips.\n *\n * Supports .tid, .json, and .meta files. Applies skinny logic:\n * - System tiddlers ($:/) → always full text\n * - Plugins (application/json + plugin-type) → always full text\n * - Module tiddlers (module-type) → always full text\n * - Small tiddlers (< 10KB body) → full text\n * - Large user tiddlers → skinny (_is_skinny: \"yes\", text omitted)\n *\n * @param filePaths Array of absolute filesystem paths\n * @param quickLoadMode If true, all tiddlers returned as skinny\n * @returns JSON string: serialized array of tiddler field objects\n */\n batchParseTidFiles(filePaths: string[], quickLoadMode: boolean): Promise<string>;\n\n /**\n * Lightweight native git status using direct git-index parsing.\n *\n * Parses `.git/index` to get tracked files and their stat-cache entries,\n * then compares against the working directory using file size and mtime.\n * Orders of magnitude faster than isomorphic-git's `statusMatrix` because:\n * - No JS↔Native bridge round-trips per file\n * - Uses stat-cache (size+mtime) instead of SHA-1 re-hashing\n * - Parallel file walking in Java\n *\n * @param gitRootDir The root directory of the git repository (parent of .git/)\n * @returns JSON string: `[{\"path\":\"tiddlers/foo.tid\",\"type\":\"add\"|\"modify\"|\"delete\"}, ...]`\n */\n gitStatus(gitRootDir: string): Promise<string>;\n\n /**\n * Debug function returning diagnostic info about the git repository state.\n * @returns JSON string with root/gitDir/index existence and git dir children\n */\n gitStatusDebug(gitRootDir: string): Promise<string>;\n\n /**\n * Build `.git/index` natively by reading the HEAD tree from pack files,\n * stat'ing all files on disk, and writing a v2 index file.\n *\n * This is used after archive clone where TidGi Desktop's tar export\n * doesn't include `.git/index`.\n *\n * @param gitRootDir The root directory of the git repository (parent of .git/)\n * @returns JSON string: `{\"ok\":true,\"entries\":N,\"indexSize\":M}` or `{\"ok\":false,\"error\":\"...\"}`\n */\n buildGitIndex(gitRootDir: string): Promise<string>;\n}\n\nexport const ExternalStorage: IExternalStorageModule = new Proxy({} as IExternalStorageModule, {\n get(_target, property) {\n const mod = getNativeModule();\n return (mod as unknown as Record<string | symbol, unknown>)[property];\n },\n});\n\n/**\n * Strip file:// prefix from a URI to produce a plain filesystem path.\n * Safe to call on paths that are already plain.\n */\nexport function toPlainPath(uriOrPath: string): string {\n if (uriOrPath.startsWith('file://')) {\n return uriOrPath.slice('file://'.length);\n }\n return uriOrPath;\n}\n\nexport type { BatchWriteResult, DownloadFileResumableResult, ExtractTarResult, FileInfo, HttpPostToFileResult, IExternalStorageModule, ReadFileChunkResult };\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAExC,IAAI,OAA2C,CAAC;AAEhD;;;;GAIG;AACH,SAAS,eAAe;IACtB,IAAI,OAAO;QAAE,OAAO,OAAO,CAAC;IAC5B,IAAI,QAAQ,CAAC,EAAE,KAAK,SAAS,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;QACvD,MAAM,IAAI,KAAK,CAAC,oEAAoE,CAAC,CAAC;IACxF,CAAC;IACD,iEAAiE;IACjE,MAAM,EAAE,mBAAmB,EAAE,GAAG,OAAO,CAAC,mBAAmB,CAAsE,CAAC;IAClI,OAAO,GAAG,mBAAmB,CAAC,iBAAiB,CAAC,CAAC;IACjD,OAAO,OAAO,CAAC;AACjB,CAAC;AA+MD,MAAM,CAAC,MAAM,eAAe,GAA2B,IAAI,KAAK,CAAC,EAA4B,EAAE;IAC7F,GAAG,CAAC,OAAO,EAAE,QAAQ;QACnB,MAAM,GAAG,GAAG,eAAe,EAAE,CAAC;QAC9B,OAAQ,GAAmD,CAAC,QAAQ,CAAC,CAAC;IACxE,CAAC;CACF,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,SAAiB;IAC3C,IAAI,SAAS,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACpC,OAAO,SAAS,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAC3C,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC","sourcesContent":["/**\n * TypeScript bindings for the ExternalStorage native module.\n *\n * This module uses raw java.io.File on Android to bypass Expo FileSystem's\n * directory whitelist. It allows reading/writing to shared external storage\n * when MANAGE_EXTERNAL_STORAGE permission is granted.\n *\n * All path arguments are plain filesystem paths (e.g. \"/storage/emulated/0/Documents/TidGi/\").\n * Do NOT pass file:// URIs — strip the scheme before calling.\n */\nimport { Platform } from 'react-native';\n\nlet _module: IExternalStorageModule | undefined;\n\n/**\n * Lazily load the native module. Wrapped in a function so that the app does NOT\n * crash at import time if the native module is missing (e.g. on iOS or when the\n * binary was built without it).\n */\nfunction getNativeModule(): IExternalStorageModule {\n if (_module) return _module;\n if (Platform.OS !== 'android' && Platform.OS !== 'ios') {\n throw new Error('ExternalStorage native module is only available on Android and iOS');\n }\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { requireNativeModule } = require('expo-modules-core') as { requireNativeModule: (name: string) => IExternalStorageModule };\n _module = requireNativeModule('ExternalStorage');\n return _module;\n}\n\ninterface FileInfo {\n exists: boolean;\n isDirectory: boolean;\n size: number;\n /** Milliseconds since epoch */\n modificationTime: number;\n}\n\ninterface BatchWriteResult {\n writtenCount: number;\n}\n\ninterface HttpPostToFileResult {\n statusCode: number;\n headers: Record<string, string>;\n bytesWritten: number;\n}\n\ninterface DownloadFileResumableResult {\n statusCode: number;\n /** Final size of the file on disk after download */\n totalBytes: number;\n /** true if the download resumed from a partial file (HTTP 206) */\n resumed: boolean;\n}\n\ninterface ExtractTarResult {\n filesExtracted: number;\n}\n\ninterface ReadFileChunkResult {\n /** Base64-encoded chunk data */\n data: string;\n bytesRead: number;\n}\n\ninterface IExternalStorageModule {\n exists(path: string): Promise<boolean>;\n getInfo(path: string): Promise<FileInfo>;\n\n mkdir(path: string): Promise<void>;\n readDir(path: string): Promise<string[]>;\n /** Recursively list all files under a directory, returning relative paths. Skips .git etc. */\n readDirRecursive(path: string): Promise<string[]>;\n rmdir(path: string): Promise<void>;\n\n readFileUtf8(path: string): Promise<string>;\n readFileBase64(path: string): Promise<string>;\n writeFileUtf8(path: string, content: string): Promise<void>;\n writeFileBase64(path: string, base64Content: string): Promise<void>;\n /**\n * Append a Base64-encoded chunk to a file, optionally truncating first.\n *\n * Designed for streaming large writes from JS in bounded-memory chunks\n * (e.g. 512 KB each) so the JVM never allocates the full file content,\n * avoiding OOM on 50+ MB git pack files.\n *\n * @param truncateFirst Pass `true` for the first chunk to create/truncate\n * the file, then `false` for subsequent chunks.\n */\n appendFileBase64(path: string, base64Content: string, truncateFirst: boolean): Promise<void>;\n writeFilesBase64(paths: string[], base64Contents: string[]): Promise<BatchWriteResult>;\n deleteFile(path: string): Promise<void>;\n\n isExternalStorageWritable(): Promise<boolean>;\n getExternalStorageDirectory(): Promise<string>;\n /** Android 11+ (API 30): check if MANAGE_EXTERNAL_STORAGE is granted. Pre-30 returns true. */\n isExternalStorageManager(): Promise<boolean>;\n\n /**\n * HTTP POST with the response body streamed directly to a file on disk,\n * **never buffering the full response in JVM/Hermes heap**.\n *\n * Designed for git-upload-pack which can return 100+ MB packfiles.\n *\n * @param url Target URL\n * @param headers HTTP headers as `{ key: value }`\n * @param bodyBase64 Request body encoded as Base64 (binary git protocol data)\n * @param destPath Plain filesystem path to write the response body to\n * @param contentType MIME type for the request body\n */\n httpPostToFile(\n url: string,\n headers: Record<string, string>,\n bodyBase64: string,\n destPath: string,\n contentType: string,\n ): Promise<HttpPostToFileResult>;\n\n /**\n * Read a chunk of a file starting at `offset` for up to `length` bytes.\n * Returns Base64-encoded data and actual bytes read.\n *\n * Use this to stream a large file into JS in bounded-memory chunks.\n */\n readFileChunk(path: string, offset: number, length: number): Promise<ReadFileChunkResult>;\n\n /**\n * Download a file via HTTP GET with resumable download support.\n *\n * If `destPath` already exists on disk (from a previous interrupted download),\n * sends `Range: bytes=<existingSize>-` to resume. The server must respond\n * with 206 Partial Content for resume to work; otherwise the file is\n * overwritten from scratch (200 response).\n *\n * @param url Target URL\n * @param headers Extra HTTP headers (e.g. Authorization, ETag)\n * @param destPath Plain filesystem path for the downloaded file\n */\n downloadFileResumable(\n url: string,\n headers: Record<string, string>,\n destPath: string,\n ): Promise<DownloadFileResumableResult>;\n\n /**\n * Extract an uncompressed tar archive to a destination directory.\n * Uses a native tar parser — no third-party dependency.\n * Supports POSIX ustar and GNU long-name extensions.\n * Validates paths to prevent directory traversal attacks.\n *\n * @param tarPath Path to the .tar file\n * @param destDir Destination directory (created if needed)\n */\n extractTar(tarPath: string, destDir: string): Promise<ExtractTarResult>;\n\n /**\n * Parse a batch of TiddlyWiki tiddler files entirely in native Kotlin.\n *\n * This is the critical performance optimization for initial wiki loading:\n * a single bridge call processes 100+ files in parallel, returning a\n * ready-to-inject JSON array string. Eliminates per-file bridge round-trips.\n *\n * Supports .tid, .json, and .meta files. Applies skinny logic:\n * - System tiddlers ($:/) → always full text\n * - Plugins (application/json + plugin-type) → always full text\n * - Module tiddlers (module-type) → always full text\n * - Small tiddlers (< 10KB body) → full text\n * - Large user tiddlers → skinny (_is_skinny: \"yes\", text omitted)\n *\n * @param filePaths Array of absolute filesystem paths\n * @param quickLoadMode If true, all tiddlers returned as skinny\n * @returns JSON string: serialized array of tiddler field objects\n */\n batchParseTidFiles(filePaths: string[], quickLoadMode: boolean): Promise<string>;\n\n /**\n * Lightweight native git status using direct git-index parsing.\n *\n * Parses `.git/index` to get tracked files and their stat-cache entries,\n * then compares against the working directory using file size and mtime.\n * Orders of magnitude faster than isomorphic-git's `statusMatrix` because:\n * - No JS↔Native bridge round-trips per file\n * - Uses stat-cache (size+mtime) instead of SHA-1 re-hashing\n * - Parallel file walking in Java\n *\n * @param gitRootDir The root directory of the git repository (parent of .git/)\n * @returns JSON string: `[{\"path\":\"tiddlers/foo.tid\",\"type\":\"add\"|\"modify\"|\"delete\"}, ...]`\n */\n gitStatus(gitRootDir: string): Promise<string>;\n\n /**\n * Debug function returning diagnostic info about the git repository state.\n * @returns JSON string with root/gitDir/index existence and git dir children\n */\n gitStatusDebug(gitRootDir: string): Promise<string>;\n\n /**\n * Build `.git/index` natively by reading the HEAD tree from pack files,\n * stat'ing all files on disk, and writing a v2 index file.\n *\n * This is used after archive clone where TidGi Desktop's tar export\n * doesn't include `.git/index`.\n *\n * @param gitRootDir The root directory of the git repository (parent of .git/)\n * @returns JSON string: `{\"ok\":true,\"entries\":N,\"indexSize\":M}` or `{\"ok\":false,\"error\":\"...\"}`\n */\n buildGitIndex(gitRootDir: string): Promise<string>;\n\n /**\n * Push local branch to remote using native JGit (efficient pack building).\n * Avoids OOM from isomorphic-git's JS-based pack construction on large repos.\n *\n * @param gitRootDir Absolute path to the git working directory\n * @param remoteName Remote name (e.g. \"origin\")\n * @param localBranch Local branch name (e.g. \"main\")\n * @param remoteBranch Remote branch ref (e.g. \"refs/heads/mobile-incoming\")\n * @param force Whether to force push\n * @param headers Optional HTTP headers as JSON string\n * @returns JSON string: `{\"ok\":true,\"updates\":[...]}` or `{\"ok\":false,\"error\":\"...\"}`\n */\n gitPush(gitRootDir: string, remoteName: string, localBranch: string, remoteBranch: string, force: boolean, headers?: string | null): Promise<string>;\n\n /**\n * Fetch from remote using native JGit (efficient pack handling).\n *\n * @param gitRootDir Absolute path to the git working directory\n * @param remoteName Remote name (e.g. \"origin\")\n * @param branch Branch to fetch\n * @param headers Optional HTTP headers as JSON string\n * @returns JSON string: `{\"ok\":true,\"updates\":[...]}` or `{\"ok\":false,\"error\":\"...\"}`\n */\n gitFetch(gitRootDir: string, remoteName: string, branch: string, headers?: string | null): Promise<string>;\n}\n\nexport const ExternalStorage: IExternalStorageModule = new Proxy({} as IExternalStorageModule, {\n get(_target, property) {\n const mod = getNativeModule();\n return (mod as unknown as Record<string | symbol, unknown>)[property];\n },\n});\n\n/**\n * Strip file:// prefix from a URI to produce a plain filesystem path.\n * Safe to call on paths that are already plain.\n */\nexport function toPlainPath(uriOrPath: string): string {\n if (uriOrPath.startsWith('file://')) {\n return uriOrPath.slice('file://'.length);\n }\n return uriOrPath;\n}\n\nexport type { BatchWriteResult, DownloadFileResumableResult, ExtractTarResult, FileInfo, HttpPostToFileResult, IExternalStorageModule, ReadFileChunkResult };\n"]}
|
package/expo-module.config.json
CHANGED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import Foundation
|
|
3
|
+
|
|
4
|
+
/// iOS implementation of the ExternalStorage native module.
|
|
5
|
+
/// Provides filesystem operations, TiddlyWiki batch parsing,
|
|
6
|
+
/// git status/index building, tar extraction, and streaming HTTP.
|
|
7
|
+
///
|
|
8
|
+
/// All path arguments are plain filesystem paths — NOT file:// URIs.
|
|
9
|
+
public class ExternalStorageModule: Module {
|
|
10
|
+
public func definition() -> ModuleDefinition {
|
|
11
|
+
Name("ExternalStorage")
|
|
12
|
+
|
|
13
|
+
// ─── Basic queries ─────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
AsyncFunction("exists") { (path: String) -> Bool in
|
|
16
|
+
FileManager.default.fileExists(atPath: path)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
AsyncFunction("getInfo") { (path: String) -> [String: Any] in
|
|
20
|
+
let fm = FileManager.default
|
|
21
|
+
guard fm.fileExists(atPath: path) else {
|
|
22
|
+
return ["exists": false, "isDirectory": false, "size": 0, "modificationTime": 0]
|
|
23
|
+
}
|
|
24
|
+
let attrs = try fm.attributesOfItem(atPath: path)
|
|
25
|
+
let isDir = (attrs[.type] as? FileAttributeType) == .typeDirectory
|
|
26
|
+
let size = (attrs[.size] as? UInt64) ?? 0
|
|
27
|
+
let mtime = (attrs[.modificationDate] as? Date)?.timeIntervalSince1970 ?? 0
|
|
28
|
+
return [
|
|
29
|
+
"exists": true,
|
|
30
|
+
"isDirectory": isDir,
|
|
31
|
+
"size": size,
|
|
32
|
+
"modificationTime": Int(mtime * 1000),
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ─── Directory operations ──────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
AsyncFunction("mkdir") { (path: String) in
|
|
39
|
+
try FileManager.default.createDirectory(
|
|
40
|
+
atPath: path,
|
|
41
|
+
withIntermediateDirectories: true,
|
|
42
|
+
attributes: nil
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
AsyncFunction("readDir") { (path: String) -> [String] in
|
|
47
|
+
let fm = FileManager.default
|
|
48
|
+
guard fm.fileExists(atPath: path) else {
|
|
49
|
+
throw NSError(domain: "ENOENT", code: 2, userInfo: [NSLocalizedDescriptionKey: "Directory does not exist: \(path)"])
|
|
50
|
+
}
|
|
51
|
+
return try fm.contentsOfDirectory(atPath: path)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
AsyncFunction("readDirRecursive") { (path: String) -> [String] in
|
|
55
|
+
Self.readDirRecursive(root: path)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
AsyncFunction("rmdir") { (path: String) in
|
|
59
|
+
let fm = FileManager.default
|
|
60
|
+
if fm.fileExists(atPath: path) {
|
|
61
|
+
try fm.removeItem(atPath: path)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── File read/write ───────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
AsyncFunction("readFileUtf8") { (path: String) -> String in
|
|
68
|
+
guard FileManager.default.fileExists(atPath: path) else {
|
|
69
|
+
throw NSError(domain: "ENOENT", code: 2, userInfo: [NSLocalizedDescriptionKey: "File does not exist: \(path)"])
|
|
70
|
+
}
|
|
71
|
+
return try String(contentsOfFile: path, encoding: .utf8)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
AsyncFunction("readFileBase64") { (path: String) -> String in
|
|
75
|
+
guard FileManager.default.fileExists(atPath: path) else {
|
|
76
|
+
throw NSError(domain: "ENOENT", code: 2, userInfo: [NSLocalizedDescriptionKey: "File does not exist: \(path)"])
|
|
77
|
+
}
|
|
78
|
+
let data = try Data(contentsOf: URL(fileURLWithPath: path))
|
|
79
|
+
return data.base64EncodedString()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
AsyncFunction("writeFileUtf8") { (path: String, content: String) in
|
|
83
|
+
let url = URL(fileURLWithPath: path)
|
|
84
|
+
try FileManager.default.createDirectory(
|
|
85
|
+
at: url.deletingLastPathComponent(),
|
|
86
|
+
withIntermediateDirectories: true,
|
|
87
|
+
attributes: nil
|
|
88
|
+
)
|
|
89
|
+
try content.write(toFile: path, atomically: true, encoding: .utf8)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
AsyncFunction("writeFileBase64") { (path: String, base64Content: String) in
|
|
93
|
+
guard let data = Data(base64Encoded: base64Content) else {
|
|
94
|
+
throw NSError(domain: "InvalidBase64", code: 1, userInfo: nil)
|
|
95
|
+
}
|
|
96
|
+
let url = URL(fileURLWithPath: path)
|
|
97
|
+
try FileManager.default.createDirectory(
|
|
98
|
+
at: url.deletingLastPathComponent(),
|
|
99
|
+
withIntermediateDirectories: true,
|
|
100
|
+
attributes: nil
|
|
101
|
+
)
|
|
102
|
+
try data.write(to: url)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
AsyncFunction("appendFileBase64") { (path: String, base64Content: String, truncateFirst: Bool) in
|
|
106
|
+
guard let data = Data(base64Encoded: base64Content) else {
|
|
107
|
+
throw NSError(domain: "InvalidBase64", code: 1, userInfo: nil)
|
|
108
|
+
}
|
|
109
|
+
let url = URL(fileURLWithPath: path)
|
|
110
|
+
if truncateFirst {
|
|
111
|
+
try FileManager.default.createDirectory(
|
|
112
|
+
at: url.deletingLastPathComponent(),
|
|
113
|
+
withIntermediateDirectories: true,
|
|
114
|
+
attributes: nil
|
|
115
|
+
)
|
|
116
|
+
try data.write(to: url)
|
|
117
|
+
} else {
|
|
118
|
+
let handle = try FileHandle(forWritingTo: url)
|
|
119
|
+
defer { handle.closeFile() }
|
|
120
|
+
handle.seekToEndOfFile()
|
|
121
|
+
handle.write(data)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
AsyncFunction("writeFilesBase64") { (paths: [String], base64Contents: [String]) -> [String: Int] in
|
|
126
|
+
guard paths.count == base64Contents.count else {
|
|
127
|
+
throw NSError(domain: "InvalidArgs", code: 1, userInfo: [NSLocalizedDescriptionKey: "paths and contents must have same length"])
|
|
128
|
+
}
|
|
129
|
+
var written = 0
|
|
130
|
+
for (path, b64) in zip(paths, base64Contents) {
|
|
131
|
+
guard let data = Data(base64Encoded: b64) else { continue }
|
|
132
|
+
let url = URL(fileURLWithPath: path)
|
|
133
|
+
try FileManager.default.createDirectory(
|
|
134
|
+
at: url.deletingLastPathComponent(),
|
|
135
|
+
withIntermediateDirectories: true,
|
|
136
|
+
attributes: nil
|
|
137
|
+
)
|
|
138
|
+
try data.write(to: url)
|
|
139
|
+
written += 1
|
|
140
|
+
}
|
|
141
|
+
return ["writtenCount": written]
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
AsyncFunction("deleteFile") { (path: String) in
|
|
145
|
+
let fm = FileManager.default
|
|
146
|
+
if fm.fileExists(atPath: path) {
|
|
147
|
+
try fm.removeItem(atPath: path)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ─── Storage queries (iOS equivalents) ─────────────────────────
|
|
152
|
+
|
|
153
|
+
AsyncFunction("isExternalStorageWritable") { () -> Bool in
|
|
154
|
+
// iOS always has writable app sandbox
|
|
155
|
+
true
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
AsyncFunction("getExternalStorageDirectory") { () -> String in
|
|
159
|
+
// Return the Documents directory
|
|
160
|
+
NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first ?? ""
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
AsyncFunction("isExternalStorageManager") { () -> Bool in
|
|
164
|
+
// iOS doesn't have this concept — always true within sandbox
|
|
165
|
+
true
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ─── Streaming HTTP operations ─────────────────────────────────
|
|
169
|
+
|
|
170
|
+
AsyncFunction("httpPostToFile") { (url: String, headersMap: [String: String], bodyBase64: String, destPath: String, contentType: String) -> [String: Any] in
|
|
171
|
+
try await Self.httpPostToFile(
|
|
172
|
+
url: url,
|
|
173
|
+
headers: headersMap,
|
|
174
|
+
bodyBase64: bodyBase64,
|
|
175
|
+
destPath: destPath,
|
|
176
|
+
contentType: contentType
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
AsyncFunction("downloadFileResumable") { (url: String, headersMap: [String: String], destPath: String) -> [String: Any] in
|
|
181
|
+
try await Self.downloadFileResumable(url: url, headers: headersMap, destPath: destPath)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ─── Chunked file reading ──────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
AsyncFunction("readFileChunk") { (path: String, offset: Int, length: Int) -> [String: Any] in
|
|
187
|
+
guard FileManager.default.fileExists(atPath: path) else {
|
|
188
|
+
throw NSError(domain: "ENOENT", code: 2, userInfo: nil)
|
|
189
|
+
}
|
|
190
|
+
let handle = try FileHandle(forReadingFrom: URL(fileURLWithPath: path))
|
|
191
|
+
defer { handle.closeFile() }
|
|
192
|
+
handle.seek(toFileOffset: UInt64(offset))
|
|
193
|
+
let data = handle.readData(ofLength: length)
|
|
194
|
+
return [
|
|
195
|
+
"data": data.base64EncodedString(),
|
|
196
|
+
"bytesRead": data.count,
|
|
197
|
+
]
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ─── Tar extraction ────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
AsyncFunction("extractTar") { (tarPath: String, destDir: String) -> [String: Int] in
|
|
203
|
+
let count = try TarExtractor.extract(tarPath: tarPath, destDir: destDir)
|
|
204
|
+
return ["filesExtracted": count]
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ─── TiddlyWiki batch parsing ──────────────────────────────────
|
|
208
|
+
|
|
209
|
+
AsyncFunction("batchParseTidFiles") { (filePaths: [String], quickLoadMode: Bool) -> String in
|
|
210
|
+
TiddlerParser.batchParse(filePaths: filePaths, quickLoadMode: quickLoadMode)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ─── Git operations ────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
AsyncFunction("gitStatus") { (gitRootDir: String) -> String in
|
|
216
|
+
try GitHelper.gitStatus(rootDir: gitRootDir)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
AsyncFunction("gitStatusDebug") { (gitRootDir: String) -> String in
|
|
220
|
+
try GitHelper.gitStatusDebug(rootDir: gitRootDir)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
AsyncFunction("buildGitIndex") { (gitRootDir: String) -> String in
|
|
224
|
+
try GitHelper.buildGitIndex(rootDir: gitRootDir)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ─── Static helpers ──────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
private static let skipDirs: Set<String> = [".git", "node_modules", ".DS_Store", "output"]
|
|
231
|
+
|
|
232
|
+
static func readDirRecursive(root: String) -> [String] {
|
|
233
|
+
var results = [String]()
|
|
234
|
+
walkDir(base: root, prefix: "", results: &results)
|
|
235
|
+
return results
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private static func walkDir(base: String, prefix: String, results: inout [String]) {
|
|
239
|
+
let fm = FileManager.default
|
|
240
|
+
guard let children = try? fm.contentsOfDirectory(atPath: base) else { return }
|
|
241
|
+
for child in children {
|
|
242
|
+
if skipDirs.contains(child) { continue }
|
|
243
|
+
let fullPath = (base as NSString).appendingPathComponent(child)
|
|
244
|
+
let relPath = prefix.isEmpty ? child : "\(prefix)/\(child)"
|
|
245
|
+
var isDir: ObjCBool = false
|
|
246
|
+
if fm.fileExists(atPath: fullPath, isDirectory: &isDir), isDir.boolValue {
|
|
247
|
+
walkDir(base: fullPath, prefix: relPath, results: &results)
|
|
248
|
+
} else {
|
|
249
|
+
results.append(relPath)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ─── Streaming HTTP helpers ──────────────────────────────────────
|
|
255
|
+
|
|
256
|
+
private static func httpPostToFile(
|
|
257
|
+
url: String,
|
|
258
|
+
headers: [String: String],
|
|
259
|
+
bodyBase64: String,
|
|
260
|
+
destPath: String,
|
|
261
|
+
contentType: String
|
|
262
|
+
) async throws -> [String: Any] {
|
|
263
|
+
guard let requestUrl = URL(string: url) else {
|
|
264
|
+
throw NSError(domain: "InvalidURL", code: 1, userInfo: nil)
|
|
265
|
+
}
|
|
266
|
+
guard let bodyData = Data(base64Encoded: bodyBase64) else {
|
|
267
|
+
throw NSError(domain: "InvalidBase64", code: 1, userInfo: nil)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let destUrl = URL(fileURLWithPath: destPath)
|
|
271
|
+
try FileManager.default.createDirectory(
|
|
272
|
+
at: destUrl.deletingLastPathComponent(),
|
|
273
|
+
withIntermediateDirectories: true,
|
|
274
|
+
attributes: nil
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
var request = URLRequest(url: requestUrl)
|
|
278
|
+
request.httpMethod = "POST"
|
|
279
|
+
request.setValue(contentType, forHTTPHeaderField: "Content-Type")
|
|
280
|
+
for (key, value) in headers {
|
|
281
|
+
request.setValue(value, forHTTPHeaderField: key)
|
|
282
|
+
}
|
|
283
|
+
request.httpBody = bodyData
|
|
284
|
+
request.timeoutInterval = 300 // 5 min read timeout
|
|
285
|
+
|
|
286
|
+
let (data, response) = try await URLSession.shared.data(for: request)
|
|
287
|
+
let httpResponse = response as? HTTPURLResponse
|
|
288
|
+
let statusCode = httpResponse?.statusCode ?? 0
|
|
289
|
+
let responseHeaders = (httpResponse?.allHeaderFields as? [String: String]) ?? [:]
|
|
290
|
+
|
|
291
|
+
// Write response to file
|
|
292
|
+
try data.write(to: destUrl)
|
|
293
|
+
|
|
294
|
+
return [
|
|
295
|
+
"statusCode": statusCode,
|
|
296
|
+
"headers": responseHeaders,
|
|
297
|
+
"bytesWritten": data.count,
|
|
298
|
+
]
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private static func downloadFileResumable(
|
|
302
|
+
url: String,
|
|
303
|
+
headers: [String: String],
|
|
304
|
+
destPath: String
|
|
305
|
+
) async throws -> [String: Any] {
|
|
306
|
+
guard let requestUrl = URL(string: url) else {
|
|
307
|
+
throw NSError(domain: "InvalidURL", code: 1, userInfo: nil)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let destUrl = URL(fileURLWithPath: destPath)
|
|
311
|
+
try FileManager.default.createDirectory(
|
|
312
|
+
at: destUrl.deletingLastPathComponent(),
|
|
313
|
+
withIntermediateDirectories: true,
|
|
314
|
+
attributes: nil
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
var request = URLRequest(url: requestUrl)
|
|
318
|
+
request.httpMethod = "GET"
|
|
319
|
+
for (key, value) in headers {
|
|
320
|
+
request.setValue(value, forHTTPHeaderField: key)
|
|
321
|
+
}
|
|
322
|
+
request.timeoutInterval = 600 // 10 min
|
|
323
|
+
|
|
324
|
+
// Check existing file for resume
|
|
325
|
+
var existingSize: UInt64 = 0
|
|
326
|
+
if FileManager.default.fileExists(atPath: destPath) {
|
|
327
|
+
let attrs = try FileManager.default.attributesOfItem(atPath: destPath)
|
|
328
|
+
existingSize = (attrs[.size] as? UInt64) ?? 0
|
|
329
|
+
if existingSize > 0 {
|
|
330
|
+
request.setValue("bytes=\(existingSize)-", forHTTPHeaderField: "Range")
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
let (data, response) = try await URLSession.shared.data(for: request)
|
|
335
|
+
let httpResponse = response as? HTTPURLResponse
|
|
336
|
+
let statusCode = httpResponse?.statusCode ?? 0
|
|
337
|
+
let resumed = statusCode == 206
|
|
338
|
+
|
|
339
|
+
if resumed {
|
|
340
|
+
// Append to existing file
|
|
341
|
+
let handle = try FileHandle(forWritingTo: destUrl)
|
|
342
|
+
defer { handle.closeFile() }
|
|
343
|
+
handle.seekToEndOfFile()
|
|
344
|
+
handle.write(data)
|
|
345
|
+
} else {
|
|
346
|
+
// Overwrite
|
|
347
|
+
try data.write(to: destUrl)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
let finalAttrs = try FileManager.default.attributesOfItem(atPath: destPath)
|
|
351
|
+
let totalBytes = (finalAttrs[.size] as? UInt64) ?? 0
|
|
352
|
+
|
|
353
|
+
return [
|
|
354
|
+
"statusCode": statusCode,
|
|
355
|
+
"totalBytes": totalBytes,
|
|
356
|
+
"resumed": resumed,
|
|
357
|
+
]
|
|
358
|
+
}
|
|
359
|
+
}
|