expo-tiddlywiki-filesystem-android-external-storage 2.0.2 → 2.1.1
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.
|
@@ -522,6 +522,111 @@ class ExternalStorageModule : Module() {
|
|
|
522
522
|
|
|
523
523
|
// ─── TiddlyWiki batch file parsing ─────────────────────────────────
|
|
524
524
|
|
|
525
|
+
/**
|
|
526
|
+
* Lightweight native git status — orders of magnitude faster than
|
|
527
|
+
* isomorphic-git's statusMatrix which must cross the JS↔Native bridge
|
|
528
|
+
* for every file read AND compute SHA-1 hashes in JavaScript.
|
|
529
|
+
*
|
|
530
|
+
* Strategy:
|
|
531
|
+
* 1. Parse `.git/index` to get the list of tracked files with their
|
|
532
|
+
* stat-cache entries (size, mtime).
|
|
533
|
+
* 2. Walk the working directory in parallel using Java NIO.
|
|
534
|
+
* 3. Compare stat-cache: if size+mtime match → file is clean.
|
|
535
|
+
* If they differ → mark as modified (we skip re-hashing since
|
|
536
|
+
* the user only needs to know *which* files changed, not the
|
|
537
|
+
* exact content delta).
|
|
538
|
+
* 4. Files in the index but missing from disk → deleted.
|
|
539
|
+
* 5. Files on disk but not in the index → added (untracked).
|
|
540
|
+
*
|
|
541
|
+
* @param gitRootDir The root directory of the git repository (parent of .git/)
|
|
542
|
+
* @return JSON string: `[{"path":"tiddlers/foo.tid","type":"add"}, ...]`
|
|
543
|
+
*/
|
|
544
|
+
AsyncFunction("gitStatus") { gitRootDir: String ->
|
|
545
|
+
val root = File(gitRootDir)
|
|
546
|
+
val gitDir = File(root, ".git")
|
|
547
|
+
if (!gitDir.exists()) {
|
|
548
|
+
throw Exception("Not a git repository: $gitRootDir (no .git directory)")
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
val indexFile = File(gitDir, "index")
|
|
552
|
+
if (!indexFile.exists()) {
|
|
553
|
+
// No index means no tracked files — everything is untracked
|
|
554
|
+
return@AsyncFunction "[]"
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// 1. Parse the git index
|
|
558
|
+
val indexEntries = parseGitIndex(indexFile)
|
|
559
|
+
android.util.Log.i("GitStatus", "Parsed ${indexEntries.size} entries from git index at $gitRootDir")
|
|
560
|
+
if (indexEntries.size <= 5) {
|
|
561
|
+
indexEntries.forEach { e -> android.util.Log.i("GitStatus", " index: ${e.path} size=${e.size} mtime=${e.mtimeSeconds}") }
|
|
562
|
+
} else {
|
|
563
|
+
indexEntries.take(3).forEach { e -> android.util.Log.i("GitStatus", " index: ${e.path} size=${e.size} mtime=${e.mtimeSeconds}") }
|
|
564
|
+
android.util.Log.i("GitStatus", " ... and ${indexEntries.size - 3} more entries")
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// 2. Walk the working directory (skip .git, node_modules, etc.)
|
|
568
|
+
val skipDirs = setOf(".git", "node_modules", "output")
|
|
569
|
+
val workdirFiles = mutableSetOf<String>()
|
|
570
|
+
fun walkDir(dir: File, prefix: String) {
|
|
571
|
+
val children = dir.listFiles() ?: return
|
|
572
|
+
for (child in children) {
|
|
573
|
+
val relPath = if (prefix.isEmpty()) child.name else "$prefix/${child.name}"
|
|
574
|
+
if (child.isDirectory) {
|
|
575
|
+
if (child.name !in skipDirs) {
|
|
576
|
+
walkDir(child, relPath)
|
|
577
|
+
}
|
|
578
|
+
} else {
|
|
579
|
+
workdirFiles.add(relPath)
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
walkDir(root, "")
|
|
584
|
+
android.util.Log.i("GitStatus", "Found ${workdirFiles.size} files on disk")
|
|
585
|
+
// Log files in tiddlers/ dir specifically
|
|
586
|
+
val tiddlerFiles = workdirFiles.filter { it.startsWith("tiddlers/") }
|
|
587
|
+
android.util.Log.i("GitStatus", " tiddlers/ count: ${tiddlerFiles.size}, sample: ${tiddlerFiles.take(5).joinToString()}")
|
|
588
|
+
|
|
589
|
+
// 3. Compare index vs working directory
|
|
590
|
+
val changes = JSONArray()
|
|
591
|
+
val indexPaths = mutableSetOf<String>()
|
|
592
|
+
|
|
593
|
+
for (entry in indexEntries) {
|
|
594
|
+
indexPaths.add(entry.path)
|
|
595
|
+
val workFile = File(root, entry.path)
|
|
596
|
+
if (!workFile.exists()) {
|
|
597
|
+
// Tracked file missing from disk → deleted
|
|
598
|
+
val obj = JSONObject()
|
|
599
|
+
obj.put("path", entry.path)
|
|
600
|
+
obj.put("type", "delete")
|
|
601
|
+
changes.put(obj)
|
|
602
|
+
} else {
|
|
603
|
+
// Check stat cache: size and mtime
|
|
604
|
+
val diskSize = workFile.length()
|
|
605
|
+
val diskMtime = workFile.lastModified() / 1000 // git index uses seconds
|
|
606
|
+
if (diskSize != entry.size || diskMtime != entry.mtimeSeconds) {
|
|
607
|
+
val obj = JSONObject()
|
|
608
|
+
obj.put("path", entry.path)
|
|
609
|
+
obj.put("type", "modify")
|
|
610
|
+
changes.put(obj)
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// 4. Files on disk but not in index → added
|
|
616
|
+
for (path in workdirFiles) {
|
|
617
|
+
if (path !in indexPaths) {
|
|
618
|
+
val obj = JSONObject()
|
|
619
|
+
obj.put("path", path)
|
|
620
|
+
obj.put("type", "add")
|
|
621
|
+
changes.put(obj)
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
changes.toString()
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// ─── TiddlyWiki batch file parsing ─────────────────────────────────
|
|
629
|
+
|
|
525
630
|
/**
|
|
526
631
|
* Parse a batch of TiddlyWiki tiddler files entirely in Kotlin.
|
|
527
632
|
*
|
|
@@ -820,4 +925,114 @@ class ExternalStorageModule : Module() {
|
|
|
820
925
|
skipped += n
|
|
821
926
|
}
|
|
822
927
|
}
|
|
928
|
+
|
|
929
|
+
// ─── Git index parser ─────────────────────────────────────────────
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Minimal representation of a git index entry — just what we need
|
|
933
|
+
* for stat-cache comparison.
|
|
934
|
+
*/
|
|
935
|
+
data class GitIndexEntry(
|
|
936
|
+
val path: String,
|
|
937
|
+
val size: Long,
|
|
938
|
+
val mtimeSeconds: Long,
|
|
939
|
+
)
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Parse a git index file (versions 2, 3, 4).
|
|
943
|
+
*
|
|
944
|
+
* Format reference: https://git-scm.com/docs/index-format
|
|
945
|
+
*
|
|
946
|
+
* We only extract the fields needed for stat-cache comparison:
|
|
947
|
+
* file path, file size, and mtime (seconds).
|
|
948
|
+
*/
|
|
949
|
+
private fun parseGitIndex(indexFile: File): List<GitIndexEntry> {
|
|
950
|
+
val bytes = indexFile.readBytes()
|
|
951
|
+
if (bytes.size < 12) return emptyList()
|
|
952
|
+
|
|
953
|
+
// Header: 4-byte signature "DIRC"
|
|
954
|
+
val sig = String(bytes, 0, 4, Charsets.US_ASCII)
|
|
955
|
+
if (sig != "DIRC") return emptyList()
|
|
956
|
+
|
|
957
|
+
// 4-byte version number
|
|
958
|
+
val version = readInt32(bytes, 4)
|
|
959
|
+
if (version !in 2..4) return emptyList()
|
|
960
|
+
|
|
961
|
+
// 4-byte number of entries
|
|
962
|
+
val entryCount = readInt32(bytes, 8)
|
|
963
|
+
val entries = ArrayList<GitIndexEntry>(entryCount)
|
|
964
|
+
|
|
965
|
+
var offset = 12 // start of first entry
|
|
966
|
+
|
|
967
|
+
for (i in 0 until entryCount) {
|
|
968
|
+
if (offset + 62 > bytes.size) break // minimum entry size
|
|
969
|
+
|
|
970
|
+
// Offset 0: 32-bit ctime seconds (skip)
|
|
971
|
+
// Offset 4: 32-bit ctime nanoseconds (skip)
|
|
972
|
+
// Offset 8: 32-bit mtime seconds
|
|
973
|
+
val mtimeSeconds = readInt32(bytes, offset + 8).toLong() and 0xFFFFFFFFL
|
|
974
|
+
// Offset 12: 32-bit mtime nanoseconds (skip)
|
|
975
|
+
// Offset 16: 32-bit dev (skip)
|
|
976
|
+
// Offset 20: 32-bit ino (skip)
|
|
977
|
+
// Offset 24: 32-bit mode (skip)
|
|
978
|
+
// Offset 28: 32-bit uid (skip)
|
|
979
|
+
// Offset 32: 32-bit gid (skip)
|
|
980
|
+
// Offset 36: 32-bit file size
|
|
981
|
+
val fileSize = readInt32(bytes, offset + 36).toLong() and 0xFFFFFFFFL
|
|
982
|
+
// Offset 40: 160-bit (20 bytes) SHA-1 (skip)
|
|
983
|
+
// Offset 60: 16-bit flags
|
|
984
|
+
val flags = readInt16(bytes, offset + 60)
|
|
985
|
+
val nameLength = flags and 0xFFF
|
|
986
|
+
|
|
987
|
+
// The path starts at offset 62
|
|
988
|
+
val pathStart = offset + 62
|
|
989
|
+
val pathEnd: Int
|
|
990
|
+
if (nameLength == 0xFFF) {
|
|
991
|
+
// Name is longer than 0xFFF — find the NUL terminator
|
|
992
|
+
var nullPos = pathStart
|
|
993
|
+
while (nullPos < bytes.size && bytes[nullPos] != 0.toByte()) nullPos++
|
|
994
|
+
pathEnd = nullPos
|
|
995
|
+
} else {
|
|
996
|
+
pathEnd = pathStart + nameLength
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
val path = if (pathEnd <= bytes.size) {
|
|
1000
|
+
String(bytes, pathStart, pathEnd - pathStart, Charsets.UTF_8)
|
|
1001
|
+
} else {
|
|
1002
|
+
break
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
entries.add(GitIndexEntry(path = path, size = fileSize, mtimeSeconds = mtimeSeconds))
|
|
1006
|
+
|
|
1007
|
+
// Entry is padded to a multiple of 8 bytes (from the start of the entry).
|
|
1008
|
+
// Total entry bytes = 62 + pathLength + 1 (NUL), rounded up to 8.
|
|
1009
|
+
if (version < 4) {
|
|
1010
|
+
val entryLength = 62 + (pathEnd - pathStart) + 1
|
|
1011
|
+
val paddedLength = (entryLength + 7) and 7.inv()
|
|
1012
|
+
offset += paddedLength
|
|
1013
|
+
} else {
|
|
1014
|
+
// Version 4 uses prefix compression — path is stored differently.
|
|
1015
|
+
// For simplicity, fall back to NUL scanning.
|
|
1016
|
+
var nextOffset = pathEnd + 1
|
|
1017
|
+
// No padding in v4
|
|
1018
|
+
offset = nextOffset
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
return entries
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/** Read a big-endian 32-bit integer from a byte array. */
|
|
1026
|
+
private fun readInt32(bytes: ByteArray, offset: Int): Int {
|
|
1027
|
+
return ((bytes[offset].toInt() and 0xFF) shl 24) or
|
|
1028
|
+
((bytes[offset + 1].toInt() and 0xFF) shl 16) or
|
|
1029
|
+
((bytes[offset + 2].toInt() and 0xFF) shl 8) or
|
|
1030
|
+
(bytes[offset + 3].toInt() and 0xFF)
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/** Read a big-endian 16-bit integer from a byte array. */
|
|
1034
|
+
private fun readInt16(bytes: ByteArray, offset: Int): Int {
|
|
1035
|
+
return ((bytes[offset].toInt() and 0xFF) shl 8) or
|
|
1036
|
+
(bytes[offset + 1].toInt() and 0xFF)
|
|
1037
|
+
}
|
|
823
1038
|
}
|
package/build/index.d.ts
CHANGED
|
@@ -119,6 +119,20 @@ interface IExternalStorageModule {
|
|
|
119
119
|
* @returns JSON string: serialized array of tiddler field objects
|
|
120
120
|
*/
|
|
121
121
|
batchParseTidFiles(filePaths: string[], quickLoadMode: boolean): Promise<string>;
|
|
122
|
+
/**
|
|
123
|
+
* Lightweight native git status using direct git-index parsing.
|
|
124
|
+
*
|
|
125
|
+
* Parses `.git/index` to get tracked files and their stat-cache entries,
|
|
126
|
+
* then compares against the working directory using file size and mtime.
|
|
127
|
+
* Orders of magnitude faster than isomorphic-git's `statusMatrix` because:
|
|
128
|
+
* - No JS↔Native bridge round-trips per file
|
|
129
|
+
* - Uses stat-cache (size+mtime) instead of SHA-1 re-hashing
|
|
130
|
+
* - Parallel file walking in Java
|
|
131
|
+
*
|
|
132
|
+
* @param gitRootDir The root directory of the git repository (parent of .git/)
|
|
133
|
+
* @returns JSON string: `[{"path":"tiddlers/foo.tid","type":"add"|"modify"|"delete"}, ...]`
|
|
134
|
+
*/
|
|
135
|
+
gitStatus(gitRootDir: string): Promise<string>;
|
|
122
136
|
}
|
|
123
137
|
export declare const ExternalStorage: IExternalStorageModule;
|
|
124
138
|
/**
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA8BA,UAAU,QAAQ;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,gBAAgB,EAAE,MAAM,CAAC;CAC1B;AAED,UAAU,gBAAgB;IACxB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,UAAU,oBAAoB;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,UAAU,2BAA2B;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,kEAAkE;IAClE,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,UAAU,gBAAgB;IACxB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,UAAU,mBAAmB;IAC3B,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,sBAAsB;IAC9B,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACvC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAEzC,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACzC,8FAA8F;IAC9F,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAClD,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEnC,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5C,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9C,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5D,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE;;;;;;;;;OASG;IACH,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7F,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,cAAc,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACvF,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAExC,yBAAyB,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAC9C,2BAA2B,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC/C,8FAA8F;IAC9F,wBAAwB,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAE7C;;;;;;;;;;;OAWG;IACH,cAAc,CACZ,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAEjC;;;;;OAKG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAE1F;;;;;;;;;;;OAWG;IACH,qBAAqB,CACnB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,2BAA2B,CAAC,CAAC;IAExC;;;;;;;;OAQG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAExE;;;;;;;;;;;;;;;;;OAiBG;IACH,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,aAAa,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
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;
|
|
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;AAoKD,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\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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-tiddlywiki-filesystem-android-external-storage",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Expo native module for TidGi-Mobile: external storage I/O + TiddlyWiki .tid/.meta/.json batch parsing in Kotlin",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
package/src/index.ts
CHANGED
|
@@ -173,6 +173,21 @@ interface IExternalStorageModule {
|
|
|
173
173
|
* @returns JSON string: serialized array of tiddler field objects
|
|
174
174
|
*/
|
|
175
175
|
batchParseTidFiles(filePaths: string[], quickLoadMode: boolean): Promise<string>;
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Lightweight native git status using direct git-index parsing.
|
|
179
|
+
*
|
|
180
|
+
* Parses `.git/index` to get tracked files and their stat-cache entries,
|
|
181
|
+
* then compares against the working directory using file size and mtime.
|
|
182
|
+
* Orders of magnitude faster than isomorphic-git's `statusMatrix` because:
|
|
183
|
+
* - No JS↔Native bridge round-trips per file
|
|
184
|
+
* - Uses stat-cache (size+mtime) instead of SHA-1 re-hashing
|
|
185
|
+
* - Parallel file walking in Java
|
|
186
|
+
*
|
|
187
|
+
* @param gitRootDir The root directory of the git repository (parent of .git/)
|
|
188
|
+
* @returns JSON string: `[{"path":"tiddlers/foo.tid","type":"add"|"modify"|"delete"}, ...]`
|
|
189
|
+
*/
|
|
190
|
+
gitStatus(gitRootDir: string): Promise<string>;
|
|
176
191
|
}
|
|
177
192
|
|
|
178
193
|
export const ExternalStorage: IExternalStorageModule = new Proxy({} as IExternalStorageModule, {
|