react-native-bspatch 1.1.3 → 2.0.0

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.
Files changed (89) hide show
  1. package/README.md +47 -52
  2. package/android/src/main/java/vn/reactnativebspatch/BsPatchModule.kt +15 -102
  3. package/android/src/main/java/vn/reactnativebspatch/OtaBundleResolver.kt +21 -1
  4. package/cpp/bsdiff/bspatch.c +1 -0
  5. package/ios/OtaBundleResolver.swift +16 -1
  6. package/ios/RNBsPatch.mm +61 -154
  7. package/lib/commonjs/NativeBsPatch.js.map +1 -1
  8. package/lib/commonjs/index.js +4 -4
  9. package/lib/commonjs/index.js.map +1 -1
  10. package/lib/commonjs/ota/OtaProvider.js +26 -22
  11. package/lib/commonjs/ota/OtaProvider.js.map +1 -1
  12. package/lib/commonjs/ota/otaAssetResolver.js +8 -2
  13. package/lib/commonjs/ota/otaAssetResolver.js.map +1 -1
  14. package/lib/commonjs/ota/otaCore.js +89 -141
  15. package/lib/commonjs/ota/otaCore.js.map +1 -1
  16. package/lib/commonjs/ota/otaLogger.js +0 -2
  17. package/lib/commonjs/ota/otaLogger.js.map +1 -1
  18. package/lib/commonjs/ota/otaMetadata.js +18 -7
  19. package/lib/commonjs/ota/otaMetadata.js.map +1 -1
  20. package/lib/commonjs/ota/storage/GitHubRawStorage.js +3 -1
  21. package/lib/commonjs/ota/storage/GitHubRawStorage.js.map +1 -1
  22. package/lib/commonjs/ota/types.js +1 -10
  23. package/lib/commonjs/ota/types.js.map +1 -1
  24. package/lib/module/NativeBsPatch.js.map +1 -1
  25. package/lib/module/index.js +1 -1
  26. package/lib/module/index.js.map +1 -1
  27. package/lib/module/ota/OtaProvider.js +26 -22
  28. package/lib/module/ota/OtaProvider.js.map +1 -1
  29. package/lib/module/ota/otaAssetResolver.js +8 -2
  30. package/lib/module/ota/otaAssetResolver.js.map +1 -1
  31. package/lib/module/ota/otaCore.js +88 -139
  32. package/lib/module/ota/otaCore.js.map +1 -1
  33. package/lib/module/ota/otaLogger.js +0 -2
  34. package/lib/module/ota/otaLogger.js.map +1 -1
  35. package/lib/module/ota/otaMetadata.js +18 -7
  36. package/lib/module/ota/otaMetadata.js.map +1 -1
  37. package/lib/module/ota/storage/GitHubRawStorage.js +3 -1
  38. package/lib/module/ota/storage/GitHubRawStorage.js.map +1 -1
  39. package/lib/module/ota/types.js +0 -9
  40. package/lib/module/ota/types.js.map +1 -1
  41. package/lib/typescript/NativeBsPatch.d.ts +3 -15
  42. package/lib/typescript/NativeBsPatch.d.ts.map +1 -1
  43. package/lib/typescript/index.d.ts +1 -1
  44. package/lib/typescript/index.d.ts.map +1 -1
  45. package/lib/typescript/ota/OtaProvider.d.ts.map +1 -1
  46. package/lib/typescript/ota/otaAssetResolver.d.ts.map +1 -1
  47. package/lib/typescript/ota/otaCore.d.ts +5 -42
  48. package/lib/typescript/ota/otaCore.d.ts.map +1 -1
  49. package/lib/typescript/ota/otaLogger.d.ts +1 -3
  50. package/lib/typescript/ota/otaLogger.d.ts.map +1 -1
  51. package/lib/typescript/ota/otaMetadata.d.ts +2 -2
  52. package/lib/typescript/ota/otaMetadata.d.ts.map +1 -1
  53. package/lib/typescript/ota/types.d.ts +18 -54
  54. package/lib/typescript/ota/types.d.ts.map +1 -1
  55. package/package.json +3 -6
  56. package/react-native-bspatch.podspec +0 -1
  57. package/src/NativeBsPatch.ts +3 -16
  58. package/src/index.ts +1 -1
  59. package/src/ota/OtaProvider.tsx +24 -23
  60. package/src/ota/otaAssetResolver.ts +5 -2
  61. package/src/ota/otaCore.ts +118 -176
  62. package/src/ota/otaLogger.ts +0 -2
  63. package/src/ota/otaMetadata.ts +18 -8
  64. package/src/ota/storage/GitHubRawStorage.ts +1 -1
  65. package/src/ota/types.ts +18 -57
  66. package/tools/build_ota.sh +356 -177
  67. package/tools/setup_project.js +95 -11
  68. package/tools/update_registry.js +24 -28
  69. package/tools/{upload_ota_custom.sh → upload_ota.sh} +33 -114
  70. package/lib/commonjs/crypto-js.d.js +0 -2
  71. package/lib/commonjs/crypto-js.d.js.map +0 -1
  72. package/lib/commonjs/ota/storage/CustomStorage.js +0 -160
  73. package/lib/commonjs/ota/storage/CustomStorage.js.map +0 -1
  74. package/lib/commonjs/ota/storage/GitHubReleaseStorage.js +0 -99
  75. package/lib/commonjs/ota/storage/GitHubReleaseStorage.js.map +0 -1
  76. package/lib/module/crypto-js.d.js +0 -2
  77. package/lib/module/crypto-js.d.js.map +0 -1
  78. package/lib/module/ota/storage/CustomStorage.js +0 -154
  79. package/lib/module/ota/storage/CustomStorage.js.map +0 -1
  80. package/lib/module/ota/storage/GitHubReleaseStorage.js +0 -93
  81. package/lib/module/ota/storage/GitHubReleaseStorage.js.map +0 -1
  82. package/lib/typescript/ota/storage/CustomStorage.d.ts +0 -12
  83. package/lib/typescript/ota/storage/CustomStorage.d.ts.map +0 -1
  84. package/lib/typescript/ota/storage/GitHubReleaseStorage.d.ts +0 -20
  85. package/lib/typescript/ota/storage/GitHubReleaseStorage.d.ts.map +0 -1
  86. package/src/crypto-js.d.ts +0 -1
  87. package/src/ota/storage/CustomStorage.ts +0 -166
  88. package/src/ota/storage/GitHubReleaseStorage.ts +0 -103
  89. package/tools/bsdiff +0 -0
@@ -50,6 +50,7 @@ function patchFile(filePath, label, patchFn) {
50
50
 
51
51
  // ── Constants ─────────────────────────────────────────────────────────────────
52
52
 
53
+ // Plain shell; escaped below for the pbxproj shellScript string literal
53
54
  const XCODE_BUNDLE_SYNC_SNIPPET = [
54
55
  '',
55
56
  '# [react-native-bspatch] Sync compiled bundle to output/ for OTA builds',
@@ -57,17 +58,78 @@ const XCODE_BUNDLE_SYNC_SNIPPET = [
57
58
  'BUNDLE_RESULT="$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/main.jsbundle"',
58
59
  '',
59
60
  'if [ -f "$BUNDLE_RESULT" ]; then',
60
- ' mkdir -p "$(dirname \\"$DEST_FILE\\")"',
61
+ ' mkdir -p "$(dirname "$DEST_FILE")"',
61
62
  ' cp "$BUNDLE_RESULT" "$DEST_FILE"',
62
63
  ' echo "Bundle synced to $DEST_FILE"',
64
+ ' if [ "$CONFIGURATION" = "Release" ]; then',
65
+ ' APP_VER="${MARKETING_VERSION:-1.0.0}"',
66
+ ' # v1 = the store build; never overwrite it (delete the file to re-capture)',
67
+ ' for FLAVOR in development staging production; do',
68
+ ' DEST_V1="$PROJECT_DIR/output/bundle_${APP_VER}_v1_ios_${FLAVOR}.hbc"',
69
+ ' if [ ! -f "$DEST_V1" ]; then',
70
+ ' cp "$BUNDLE_RESULT" "$DEST_V1"',
71
+ ' echo "[react-native-bspatch] ✓ Auto-captured iOS base bundle v1 ($FLAVOR)"',
72
+ ' fi',
73
+ ' done',
74
+ ' fi',
63
75
  'else',
64
76
  ' echo "Bundle not found at $BUNDLE_RESULT (skipping sync)"',
65
77
  'fi',
66
- ].join('\\n');
78
+ ].join('\n').replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
67
79
 
68
80
  // The escaped version as it appears in pbxproj shellScript value
69
81
  const XCODE_BUNDLE_SYNC_MARKER = 'react-native-bspatch] Sync compiled bundle to output/';
70
82
 
83
+ const ANDROID_GRADLE_MARKER = '[react-native-bspatch] Auto-capture Base Bundle';
84
+ const ANDROID_GRADLE_SNIPPET = `
85
+ // [react-native-bspatch] Auto-capture Base Bundle on Release builds
86
+ afterEvaluate {
87
+ android.applicationVariants.all { variant ->
88
+ if (variant.buildType.name == 'release') {
89
+ def flavorName = (variant.flavorName ?: 'development').toLowerCase()
90
+ if (flavorName == 'dev') flavorName = 'development'
91
+ if (flavorName == 'prod') flavorName = 'production'
92
+ def versionName = variant.versionName ?: '1.0.0'
93
+ def capVariant = variant.name.capitalize()
94
+ def bundleTask = tasks.findByName("createBundle\${capVariant}JsAndAssets")
95
+ if (bundleTask != null) {
96
+ def saveTask = tasks.register("saveOtaBaseBundle\${capVariant}") {
97
+ doLast {
98
+ def bundleDir = file("\$buildDir/generated/assets/react/\${variant.name}")
99
+ def bundleFile = file("\$bundleDir/index.android.bundle")
100
+ def outputDir = file("\$rootDir/output")
101
+ // v1 = the store build; never overwrite it (delete the file to re-capture).
102
+ // build_ota.sh runs this same bundle task for every OTA build.
103
+ def destFile = file("\$outputDir/bundle_\${versionName}_v1_android_\${flavorName}.hbc")
104
+ if (bundleFile.exists() && !destFile.exists()) {
105
+ outputDir.mkdirs()
106
+ def destName = "bundle_\${versionName}_v1_android_\${flavorName}.hbc"
107
+ copy {
108
+ from bundleFile
109
+ into outputDir
110
+ rename { destName }
111
+ }
112
+ println "[react-native-bspatch] ✓ Auto-captured base bundle: android/output/\${destName}"
113
+ }
114
+ }
115
+ }
116
+ bundleTask.finalizedBy(saveTask)
117
+ }
118
+ }
119
+ }
120
+ }
121
+ `;
122
+
123
+ function patchAppBuildGradle(content) {
124
+ const markerIdx = content.indexOf('// ' + ANDROID_GRADLE_MARKER);
125
+ if (markerIdx === -1) return content.trimEnd() + '\n' + ANDROID_GRADLE_SNIPPET;
126
+
127
+ // Replace an older snippet in place: from the marker to the afterEvaluate block's closing brace
128
+ const end = findClosingParen(content, content.indexOf('{', content.indexOf('afterEvaluate', markerIdx)), '{', '}');
129
+ if (end === -1) return content;
130
+ return content.slice(0, markerIdx) + ANDROID_GRADLE_SNIPPET.trim() + content.slice(end + 1);
131
+ }
132
+
71
133
  const IOS_IMPORT = 'import react_native_bspatch';
72
134
  const IOS_BUNDLE_URL_OVERRIDE = `
73
135
  override func bundleURL() -> URL? {
@@ -85,13 +147,13 @@ const IOS_BUNDLE_URL_OVERRIDE = `
85
147
  const ANDROID_IMPORT = 'import vn.reactnativebspatch.OtaBundleResolver';
86
148
  const ANDROID_JS_BUNDLE = 'jsBundleFilePath = OtaBundleResolver.resolve(applicationContext)';
87
149
 
88
- function findClosingParen(source, openParenIdx) {
150
+ function findClosingParen(source, openParenIdx, open = '(', close = ')') {
89
151
  let depth = 0;
90
152
  for (let i = openParenIdx; i < source.length; i += 1) {
91
153
  const char = source[i];
92
- if (char === '(') {
154
+ if (char === open) {
93
155
  depth += 1;
94
- } else if (char === ')') {
156
+ } else if (char === close) {
95
157
  depth -= 1;
96
158
  if (depth === 0) {
97
159
  return i;
@@ -104,8 +166,14 @@ function findClosingParen(source, openParenIdx) {
104
166
  // ── iOS: project.pbxproj ─────────────────────────────────────────────────────
105
167
 
106
168
  function patchPbxproj(content) {
107
- // Already patched?
108
- if (content.includes(XCODE_BUNDLE_SYNC_MARKER)) return content;
169
+ if (content.includes(XCODE_BUNDLE_SYNC_SNIPPET)) return content;
170
+
171
+ // Older snippet present → replace it in place (it lacked the base bundle auto-capture)
172
+ const oldIdx = content.indexOf('\\n# [' + XCODE_BUNDLE_SYNC_MARKER);
173
+ if (oldIdx !== -1) {
174
+ const endIdx = content.indexOf('";', oldIdx);
175
+ return content.slice(0, oldIdx) + XCODE_BUNDLE_SYNC_SNIPPET + content.slice(endIdx);
176
+ }
109
177
 
110
178
  // Search by phase name
111
179
  const nameIdx = content.indexOf('name = "Bundle React Native code and images";');
@@ -275,12 +343,28 @@ function main() {
275
343
  patchFile(mainAppFiles[0], 'MainApplication.kt', patchMainApplication);
276
344
  }
277
345
 
346
+ const appGradleFiles = findFile(path.join(ANDROID_DIR, 'app'), (name) => name === 'build.gradle');
347
+ if (appGradleFiles.length === 0) {
348
+ console.log(' ⚠ No android/app/build.gradle found.');
349
+ } else {
350
+ patchFile(appGradleFiles[0], 'android/app/build.gradle (auto-capture base bundle)', patchAppBuildGradle);
351
+ }
352
+
353
+ // Ensure android/output/ exists
354
+ const androidOutputDir = path.join(ANDROID_DIR, 'output');
355
+ if (!fs.existsSync(androidOutputDir)) {
356
+ fs.mkdirSync(androidOutputDir, { recursive: true });
357
+ console.log(' ✓ Created android/output/ directory');
358
+ }
359
+
278
360
  console.log('\n=== Setup Complete ===');
279
361
  console.log('Next steps:');
280
- console.log(' 1. iOS: Run Xcode build once to populate ios/output/main.jsbundle');
281
- console.log(' (or run: yarn ota:init --platform ios)');
282
- console.log(' 2. Android: Run: yarn ota:init --platform android');
283
- console.log(' 3. Then: yarn ota:build to start publishing OTA updates\n');
362
+ console.log(' 1. Base bundles will now automatically be captured on Release builds (Gradle & Xcode).');
363
+ console.log(' 2. Or seed directly from an existing Store binary:');
364
+ console.log(' yarn react-native-bspatch seed --apk path/to/release.apk');
365
+ console.log(' yarn react-native-bspatch seed --ipa path/to/release.ipa');
366
+ console.log(' 3. Or create base release manually: yarn ota:init');
367
+ console.log(' 4. Then publish OTA updates safely with: yarn ota:build\n');
284
368
  }
285
369
 
286
370
  main();
@@ -85,35 +85,30 @@ if (newPatches.length > 0) {
85
85
  // We merge patches by platform to avoid overwriting between Android and iOS
86
86
  const otherPlatformPatches = flavorGroup.patches.filter(p => p.platform !== platform);
87
87
 
88
- const mappedNewPatches = newPatches.map(p => {
89
- // Naming convention from build_ota.sh
90
- const fullBundleName = `bundle_${appVersion}_v${toOtaVersion}_${platform}_${flavor}.hbc`;
91
- const manifestName = path.basename(_assetManifestFile);
92
-
93
- let signature = undefined;
94
- if (signingKey) {
95
- try {
96
- const patchFileCandidates = [
97
- path.join(path.dirname(registryPath), p.file),
98
- path.join(process.cwd(), p.file),
99
- ];
100
- let patchData = null;
101
- for (const pfc of patchFileCandidates) {
102
- if (fs.existsSync(pfc)) {
103
- patchData = fs.readFileSync(pfc);
104
- break;
105
- }
106
- }
107
- if (patchData) {
108
- const signer = crypto.createSign('SHA256');
109
- signer.update(patchData);
110
- signature = signer.sign(signingKey, 'base64');
111
- console.log(` ✓ Signed patch ${p.file} with asymmetric private key`);
112
- }
113
- } catch (signErr) {
114
- console.warn(` ⚠ Could not sign patch ${p.file}: ${signErr.message}`);
115
- }
88
+ // Patches and bundles live next to the asset manifest (android/output or ios/output)
89
+ const outputDir = path.dirname(_assetManifestFile);
90
+ const fullBundleName = `bundle_${appVersion}_v${toOtaVersion}_${platform}_${flavor}.hbc`;
91
+ const manifestName = path.basename(_assetManifestFile);
92
+
93
+ const sign = (file) => {
94
+ if (!signingKey) return undefined;
95
+ const filePath = path.join(outputDir, file);
96
+ if (!fs.existsSync(filePath)) {
97
+ console.error(` ❌ Cannot sign ${file}: not found at ${filePath}`);
98
+ process.exit(1);
116
99
  }
100
+ const signer = crypto.createSign('SHA256');
101
+ signer.update(fs.readFileSync(filePath));
102
+ console.log(` ✓ Signed ${file}`);
103
+ return signer.sign(signingKey, 'base64');
104
+ };
105
+
106
+ // Signature of the resulting bundle: verified by the client on BOTH the patch
107
+ // path and the full-bundle fallback, so a tampered registry can't swap bundles.
108
+ const bundleSignature = sign(fullBundleName);
109
+
110
+ const mappedNewPatches = newPatches.map(p => {
111
+ const signature = sign(p.file);
117
112
 
118
113
  const patchObj = {
119
114
  id: `v${p.from}-to-v${p.to}-${flavor}-${platform}`,
@@ -132,6 +127,7 @@ if (newPatches.length > 0) {
132
127
 
133
128
  if (signature) {
134
129
  patchObj.signature = signature;
130
+ patchObj.bundleSignature = bundleSignature;
135
131
  }
136
132
 
137
133
  return patchObj;
@@ -1,7 +1,7 @@
1
1
  #!/bin/bash
2
2
 
3
3
  # ─────────────────────────────────────────────────────────────────────────────
4
- # Custom OTA Upload Script for Sync Server & GitHub Storage
4
+ # OTA Upload Script — GitHub storage repository
5
5
  # Uploads: 1) All .patch & .hbc files → {baseFolder}/{appVersion}/{channel}/
6
6
  # 2) ota_registry.json → {baseFolder}/ota_registry.json
7
7
  # ─────────────────────────────────────────────────────────────────────────────
@@ -9,12 +9,10 @@
9
9
  # Load environment variables if .env exists
10
10
  ROOT_DIR=$(pwd)
11
11
  if [ -f "$ROOT_DIR/.env" ]; then
12
- export $(grep -v '^#' "$ROOT_DIR/.env" | grep -v '^$' | xargs 2>/dev/null)
12
+ set -a; . "$ROOT_DIR/.env"; set +a
13
13
  fi
14
14
 
15
15
  BASE_URL="${OTA_BASE_URL:-}"
16
- UPCODE_USER="${OTA_UPLOAD_USER:-}"
17
- UPCODE_PASS="${OTA_UPLOAD_PASSWORD:-}"
18
16
  BASE_FOLDER="${OTA_FOLDER:-${6:-ota}}"
19
17
  APP_VERSION="$1"
20
18
  CHANNEL="$2"
@@ -29,7 +27,7 @@ if [[ "$PLATFORM" == "ios" ]]; then
29
27
  fi
30
28
 
31
29
  echo ""
32
- echo "=== Custom OTA Upload | App: $APP_VERSION | Channel: $CHANNEL | V$OTA_VERSION | Platform: $PLATFORM ==="
30
+ echo "=== OTA Upload | App: $APP_VERSION | Channel: $CHANNEL | V$OTA_VERSION | Platform: $PLATFORM ==="
33
31
  echo " Output dir: $OUTPUT_DIR"
34
32
 
35
33
  # Validate
@@ -50,11 +48,23 @@ for ARG in "$@"; do
50
48
  fi
51
49
  done
52
50
 
51
+ # Files of this app version + channel only (bundle_/patch_/asset-manifest_ carry both;
52
+ # base-assets_<platform>_<channel>.json carries just the channel)
53
+ is_release_file() {
54
+ local FNAME="$1"
55
+ if [[ "$FNAME" == "base-assets_"*"_${CHANNEL}.json" ]]; then return 0; fi
56
+ [[ "$FNAME" == *"_${APP_VERSION}_"* ]] && [[ "$FNAME" == *"_${CHANNEL}."* || "$FNAME" == *"_${CHANNEL}_"* ]]
57
+ }
58
+
53
59
  # ── GitHub Auto-Push Integration ──────────────────────────────────────────────
54
- if [[ "$BASE_URL" == *"github"* ]] || [[ "$BASE_URL" == *"raw.githubusercontent"* ]]; then
60
+ if [[ "$BASE_URL" != "https://raw.githubusercontent.com/"* ]]; then
61
+ echo "ERROR: OTA_BASE_URL must be https://raw.githubusercontent.com/<owner>/<repo>/<branch>"
62
+ exit 1
63
+ fi
64
+
55
65
  # Parse OWNER, REPO, BRANCH from URL
56
66
  # Example: https://raw.githubusercontent.com/hungbv-taureau/baseform-storage/main
57
- CLEAN_URL=$(echo "$BASE_URL" | sed 's#https://raw.githubusercontent.com/##' | sed 's#https://github.com/##')
67
+ CLEAN_URL="${BASE_URL#https://raw.githubusercontent.com/}"
58
68
  OWNER=$(echo "$CLEAN_URL" | cut -d'/' -f1)
59
69
  REPO=$(echo "$CLEAN_URL" | cut -d'/' -f2)
60
70
  BRANCH=$(echo "$CLEAN_URL" | cut -d'/' -f3)
@@ -65,8 +75,11 @@ if [[ "$BASE_URL" == *"github"* ]] || [[ "$BASE_URL" == *"raw.githubusercontent"
65
75
  TOKEN="${GH_CLI_TOKEN:-${GH_TOKEN:-${GITHUB_TOKEN:-}}}"
66
76
  if [[ "$TOKEN" == "baseform" ]]; then TOKEN=""; fi
67
77
 
78
+ # Token goes in a per-command header, never into the clone's .git/config
79
+ GIT_AUTH=()
68
80
  if [[ -n "$TOKEN" ]]; then
69
- GIT_REMOTE="https://x-access-token:${TOKEN}@github.com/${OWNER}/${REPO}.git"
81
+ GIT_REMOTE="https://github.com/${OWNER}/${REPO}.git"
82
+ GIT_AUTH=(-c "http.https://github.com/.extraheader=AUTHORIZATION: basic $(printf 'x-access-token:%s' "$TOKEN" | base64 | tr -d '\n')")
70
83
  echo " Using authenticated gh CLI token for ${OWNER}/${REPO}"
71
84
  else
72
85
  GIT_REMOTE="git@github.com:${OWNER}/${REPO}.git"
@@ -83,19 +96,21 @@ if [[ "$BASE_URL" == *"github"* ]] || [[ "$BASE_URL" == *"raw.githubusercontent"
83
96
  if [ -d "$GH_REPO_DIR/.git" ]; then
84
97
  echo " Syncing local storage clone with remote..."
85
98
  git -C "$GH_REPO_DIR" remote set-url origin "$GIT_REMOTE" 2>/dev/null || true
86
- git -C "$GH_REPO_DIR" fetch origin "$BRANCH" 2>/dev/null || true
99
+ git "${GIT_AUTH[@]}" -C "$GH_REPO_DIR" fetch origin "$BRANCH" 2>/dev/null || true
87
100
  git -C "$GH_REPO_DIR" checkout "$BRANCH" 2>/dev/null || true
88
101
  git -C "$GH_REPO_DIR" reset --hard "origin/$BRANCH" 2>/dev/null || true
89
102
  else
90
103
  echo " Cloning storage repository..."
91
104
  rm -rf "$GH_REPO_DIR"
92
- git clone --branch "$BRANCH" --depth 1 "$GIT_REMOTE" "$GH_REPO_DIR" 2>/dev/null || git clone "$GIT_REMOTE" "$GH_REPO_DIR"
105
+ git "${GIT_AUTH[@]}" clone --branch "$BRANCH" --depth 1 "$GIT_REMOTE" "$GH_REPO_DIR" 2>/dev/null || git "${GIT_AUTH[@]}" clone "$GIT_REMOTE" "$GH_REPO_DIR" || exit 1
106
+ chmod 700 "$GH_REPO_DIR"
93
107
  fi
94
108
 
95
109
  # 2. Copy generated files into storage repo (or reset remote)
96
110
  if [ "$CLEAN_REMOTE" = true ]; then
97
- echo " Resetting remote storage state for channel '$CHANNEL'..."
98
- rm -rf "$GH_REPO_DIR/$BASE_FOLDER/$APP_VERSION/$CHANNEL"
111
+ echo " Resetting remote storage state for channel '$CHANNEL' (base bundle v1 + base-assets kept)..."
112
+ find "$GH_REPO_DIR/$BASE_FOLDER/$APP_VERSION/$CHANNEL" -type f \
113
+ -not -name "bundle_${APP_VERSION}_v1_*" -not -name 'base-assets_*' -delete 2>/dev/null || true
99
114
  mkdir -p "$GH_REPO_DIR/$BASE_FOLDER"
100
115
  cp "$REGISTRY_FILE" "$GH_REPO_DIR/$BASE_FOLDER/ota_registry.json"
101
116
  cp "$REGISTRY_FILE" "$GH_REPO_DIR/ota_registry.json" 2>/dev/null || true
@@ -104,18 +119,10 @@ if [[ "$BASE_URL" == *"github"* ]] || [[ "$BASE_URL" == *"raw.githubusercontent"
104
119
  mkdir -p "$TARGET_RELEASE_DIR"
105
120
 
106
121
  echo " Copying release files to storage repo for channel '$CHANNEL'..."
107
- for FILE in "$OUTPUT_DIR"/*_${CHANNEL}.json "$OUTPUT_DIR"/*_${CHANNEL}.hbc "$OUTPUT_DIR"/*_${CHANNEL}_*.patch "$OUTPUT_DIR"/*_${CHANNEL}.patch; do
122
+ for FILE in "$OUTPUT_DIR"/*.{patch,hbc,json}; do
108
123
  [[ -f "$FILE" ]] || continue
109
124
  FNAME=$(basename "$FILE")
110
- if [[ "$FNAME" == "patch_"* ]] || [[ "$FNAME" == "bundle_"* ]]; then
111
- if [[ "$FNAME" != *"_${APP_VERSION}_"* ]]; then
112
- continue
113
- fi
114
- fi
115
-
116
- if [[ "$FNAME" != *"_${APP_VERSION}_"* ]] && [[ "$FNAME" != *"_${CHANNEL}_"* ]] && [[ "$FNAME" != "asset-manifest"* ]] && [[ "$FNAME" != "base-assets"* ]]; then
117
- continue
118
- fi
125
+ is_release_file "$FNAME" || continue
119
126
  cp "$FILE" "$TARGET_RELEASE_DIR/$FNAME"
120
127
  done
121
128
 
@@ -148,102 +155,14 @@ if [[ "$BASE_URL" == *"github"* ]] || [[ "$BASE_URL" == *"raw.githubusercontent"
148
155
  git commit -m "$COMMIT_MSG"
149
156
  echo " Pushing to GitHub (${BRANCH})..."
150
157
 
151
- if git push origin "$BRANCH"; then
158
+ if git "${GIT_AUTH[@]}" push origin "$BRANCH"; then
152
159
  echo " ✓ Successfully pushed OTA release to GitHub!"
153
160
  else
154
- echo " ⚠ Git push failed. Please verify write permissions for ${OWNER}/${REPO}."
161
+ # Usually someone else pushed first: re-run so the registry is re-synced, never force-push
162
+ echo " ❌ Git push failed (remote changed or no write access to ${OWNER}/${REPO}). Re-run the build."
163
+ exit 1
155
164
  fi
156
165
  fi
157
166
  cd "$ROOT_DIR"
158
167
  echo ""
159
168
  echo "=== GitHub Sync Complete ==="
160
- exit 0
161
- fi
162
-
163
- # ── MinIO Server Integration ──────────────────────────────────────────────────
164
- echo "Logging in to MinIO Server..."
165
- LOGIN_RES=$(curl -s -X "POST" "$BASE_URL/public/authen/login" \
166
- -H 'Content-Type: application/json; charset=utf-8' \
167
- -d "{\"username\": \"$UPCODE_USER\", \"password\": \"$UPCODE_PASS\"}")
168
-
169
- TOKEN=$(echo "$LOGIN_RES" | node -e "
170
- try {
171
- const r = JSON.parse(require('fs').readFileSync(0, 'utf8'));
172
- console.log(r.data?.accessTokenInfo?.accessToken || '');
173
- } catch(e) { console.log(''); }
174
- ")
175
- if [[ -z "$TOKEN" ]]; then
176
- echo " ⚠ Notice: Remote MinIO server login skipped."
177
- echo " ✓ Bundles & registry generated at: $OUTPUT_DIR"
178
- echo " Please commit and push local changes to your GitHub storage repository."
179
- exit 0
180
- fi
181
- echo " ✓ Login successful"
182
-
183
- remote_delete() {
184
- local path=$1
185
- local encoded
186
- encoded=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$path'))")
187
- curl -s -X "DELETE" "$BASE_URL/api/minio/delete?path=$encoded" \
188
- -H "Authorization: Bearer $TOKEN" > /dev/null
189
- }
190
-
191
- remote_upload() {
192
- local local_file=$1
193
- local remote_path=$2
194
- echo " uploading: $remote_path"
195
- local RES
196
- RES=$(curl -s -X "POST" "$BASE_URL/api/minio/upload" \
197
- -H "Authorization: Bearer $TOKEN" \
198
- -H 'Content-Type: multipart/form-data' \
199
- -F "file=@$local_file" \
200
- -F "path=$remote_path")
201
- if [[ "$RES" != *"\"success\":true"* ]] && [[ "$RES" != *"\"code\":1"* ]] && [[ "$RES" != *"\"status\":200"* ]] && [[ "$RES" != *"\"code\":200"* ]]; then
202
- echo " ⚠ Upload potential issue: $RES"
203
- else
204
- echo " ✓ Upload success"
205
- fi
206
- }
207
-
208
- echo "Uploading files for channel: $CHANNEL..."
209
- FILE_COUNT=0
210
- for FILE in "$OUTPUT_DIR"/*.{patch,hbc,json}; do
211
- [[ -f "$FILE" ]] || continue
212
- FNAME=$(basename "$FILE")
213
- if [[ "$FNAME" == "patch_"* ]] || [[ "$FNAME" == "bundle_"* ]]; then
214
- if [[ "$FNAME" != *"_${APP_VERSION}_"* ]]; then
215
- continue
216
- fi
217
- fi
218
-
219
- if [[ "$FNAME" != *"_${APP_VERSION}_"* ]] && [[ "$FNAME" != *"_${CHANNEL}_"* ]] && [[ "$FNAME" != *"_${PLATFORM}_"* ]] && [[ "$FNAME" != "asset-manifest"* ]] && [[ "$FNAME" != "base-assets"* ]]; then
220
- continue
221
- fi
222
-
223
- REMOTE_PATH="$BASE_FOLDER/$APP_VERSION/$CHANNEL/$FNAME"
224
- remote_delete "$REMOTE_PATH"
225
- remote_upload "$FILE" "$REMOTE_PATH"
226
- FILE_COUNT=$((FILE_COUNT + 1))
227
- done
228
-
229
- if [[ -d "$OUTPUT_DIR/asset-store" ]]; then
230
- echo "Uploading hashed assets..."
231
- for FILE in "$OUTPUT_DIR/asset-store"/*; do
232
- [[ -f "$FILE" ]] || continue
233
- FNAME=$(basename "$FILE")
234
- REMOTE_PATH="$BASE_FOLDER/assets/$FNAME"
235
- remote_upload "$FILE" "$REMOTE_PATH"
236
- FILE_COUNT=$((FILE_COUNT + 1))
237
- done
238
- fi
239
- echo " ✓ $FILE_COUNT file(s) uploaded (patches + bundles + assets)"
240
-
241
- echo "Uploading ota_registry.json..."
242
- REMOTE_REGISTRY="$BASE_FOLDER/ota_registry.json"
243
- remote_delete "$REMOTE_REGISTRY"
244
- remote_upload "$REGISTRY_FILE" "$REMOTE_REGISTRY"
245
- echo " ✓ Registry uploaded"
246
-
247
- echo ""
248
- echo "=== Upload Complete ==="
249
- echo " Registry : $REMOTE_REGISTRY"
@@ -1,2 +0,0 @@
1
- "use strict";
2
- //# sourceMappingURL=crypto-js.d.js.map
@@ -1 +0,0 @@
1
- {"version":3,"names":[],"sourceRoot":"../../src","sources":["crypto-js.d.ts"],"mappings":"","ignoreList":[]}
@@ -1,160 +0,0 @@
1
- "use strict";
2
-
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- exports.CustomStorageProvider = void 0;
7
- var _reactNative = require("react-native");
8
- var _reactNativeBlobUtil = _interopRequireDefault(require("react-native-blob-util"));
9
- function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
10
- /** Cache tokens per server endpoint so switching configs invalidates stale tokens. */
11
- const tokenCache = new Map();
12
- /** Cache registry ETag and parsed patches to avoid redownloading unmodified manifests. */
13
- const registryCache = new Map();
14
-
15
- /**
16
- * Implementation for the custom MinIO-based storage server.
17
- */
18
- class CustomStorageProvider {
19
- getEndpoint(config, key, fallback) {
20
- return config.customServer?.endpoints?.[key] ?? fallback;
21
- }
22
- async getAccessToken(config) {
23
- if (!config.customServer) {
24
- throw new Error('customServer configuration missing');
25
- }
26
- const {
27
- baseUrl,
28
- username,
29
- password
30
- } = config.customServer;
31
- const cacheKey = `${baseUrl}|${username}`;
32
- const now = Date.now();
33
- const cached = tokenCache.get(cacheKey);
34
- if (cached && now < cached.expiresAt - 30000) {
35
- return cached.token;
36
- }
37
- const loginPath = this.getEndpoint(config, 'login', 'public/authen/login');
38
- const loginUrl = baseUrl.endsWith('/') ? `${baseUrl}${loginPath}` : `${baseUrl}/${loginPath}`;
39
- try {
40
- const res = await _reactNativeBlobUtil.default.fetch('POST', loginUrl, {
41
- 'Content-Type': 'application/json; charset=utf-8'
42
- }, JSON.stringify({
43
- username,
44
- password
45
- }));
46
- const json = JSON.parse(res.data);
47
- const token = json.data?.accessTokenInfo?.accessToken;
48
- if (!token) throw new Error('Login failed: token not found');
49
- tokenCache.set(cacheKey, {
50
- token,
51
- expiresAt: now + 3600000
52
- }); // 1 hour
53
- return token;
54
- } catch (error) {
55
- throw new Error(`Authentication failed: ${error.message}`);
56
- }
57
- }
58
- async fetchManifest(config) {
59
- if (!config.customServer) return config.bundledPatches || [];
60
- const {
61
- baseUrl,
62
- channel,
63
- baseFolder = 'bspatch'
64
- } = config.customServer;
65
- const registryPath = `${baseFolder}/ota_registry.json`;
66
- const regCacheKey = `${baseUrl}|${registryPath}|${config.appVersion}|${channel}`;
67
- const cachedReg = registryCache.get(regCacheKey);
68
- try {
69
- const token = await this.getAccessToken(config);
70
- const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
71
- const genLinkPath = this.getEndpoint(config, 'genDownloadLink', 'api/minio/gen-download-link');
72
- const genLinkUrl = `${normalizedBaseUrl}/${genLinkPath}?pathFile=${encodeURIComponent(registryPath)}`;
73
- const linkRes = await _reactNativeBlobUtil.default.fetch('GET', genLinkUrl, {
74
- Authorization: `Bearer ${token}`
75
- });
76
- const linkJson = JSON.parse(linkRes.data);
77
- const registryUrl = linkJson.data;
78
- if (!registryUrl) throw new Error('Failed to generate registry download link');
79
- const reqHeaders = {};
80
- if (cachedReg?.etag) {
81
- reqHeaders['If-None-Match'] = cachedReg.etag;
82
- }
83
- const registryRes = await _reactNativeBlobUtil.default.fetch('GET', registryUrl, reqHeaders);
84
- if (registryRes.respInfo.status === 304 && cachedReg) {
85
- return cachedReg.patches;
86
- }
87
- if (registryRes.respInfo.status !== 200) {
88
- throw new Error(`Registry fetch failed with status ${registryRes.respInfo.status}`);
89
- }
90
- const etag = registryRes.respInfo.headers?.['etag'] || registryRes.respInfo.headers?.['ETag'];
91
- if (!config.appVersion) {
92
- throw new Error('[CustomStorage] config.appVersion is required to query OTA registry');
93
- }
94
- const registry = JSON.parse(registryRes.data);
95
- const appGroup = registry?.apps?.[config.appVersion];
96
- const flavorGroup = appGroup?.flavors?.[channel.toLowerCase()];
97
- if (!flavorGroup || !flavorGroup.patches?.length) {
98
- return config.bundledPatches || [];
99
- }
100
- const matchedPatches = (flavorGroup.patches || []).filter(p => p.platform === _reactNative.Platform.OS);
101
- registryCache.set(regCacheKey, {
102
- etag,
103
- patches: matchedPatches
104
- });
105
- return matchedPatches;
106
- } catch (error) {
107
- console.warn('[CustomStorage] Manifest fetch error:', error.message);
108
- return cachedReg?.patches || config.bundledPatches || [];
109
- }
110
- }
111
- async getUpdateFileUrl(filename, config) {
112
- if (!config.customServer) throw new Error('customServer config missing');
113
- const {
114
- baseUrl,
115
- baseFolder = 'bspatch',
116
- useMinio
117
- } = config.customServer;
118
- const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
119
- const channel = config.customServer.channel.toLowerCase();
120
- const remotePath = `${baseFolder}/${config.appVersion}/${channel}/${filename}`;
121
- if (useMinio) {
122
- const token = await this.getAccessToken(config);
123
- const genLinkPath = this.getEndpoint(config, 'genDownloadLink', 'api/minio/gen-download-link');
124
- const genLinkUrl = `${normalizedBaseUrl}/${genLinkPath}?pathFile=${encodeURIComponent(remotePath)}`;
125
- const res = await _reactNativeBlobUtil.default.fetch('GET', genLinkUrl, {
126
- Authorization: `Bearer ${token}`
127
- });
128
- const json = JSON.parse(res.data);
129
- if (!json.data) throw new Error(`Failed to generate link for: ${filename}`);
130
- return json.data;
131
- }
132
- const downloadPath = this.getEndpoint(config, 'downloadFile', 'files/downloadStreamFile');
133
- return `${normalizedBaseUrl}/${downloadPath}?filepath=${encodeURIComponent(remotePath)}`;
134
- }
135
- async getAssetUrl(hash, ext, config) {
136
- if (!config.customServer) return '';
137
- const {
138
- baseUrl,
139
- baseFolder = 'bspatch',
140
- useMinio
141
- } = config.customServer;
142
- const normalizedBaseUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
143
- const filename = `${hash}.${ext}`;
144
- const remotePath = `${baseFolder}/assets/${filename}`;
145
- if (useMinio) {
146
- const token = await this.getAccessToken(config);
147
- const genLinkPath = this.getEndpoint(config, 'genDownloadLink', 'api/minio/gen-download-link');
148
- const genLinkUrl = `${normalizedBaseUrl}/${genLinkPath}?pathFile=${encodeURIComponent(remotePath)}`;
149
- const res = await _reactNativeBlobUtil.default.fetch('GET', genLinkUrl, {
150
- Authorization: `Bearer ${token}`
151
- });
152
- const json = JSON.parse(res.data);
153
- return json.data || '';
154
- }
155
- const downloadPath = this.getEndpoint(config, 'downloadFile', 'files/downloadStreamFile');
156
- return `${normalizedBaseUrl}/${downloadPath}?filepath=${encodeURIComponent(remotePath)}`;
157
- }
158
- }
159
- exports.CustomStorageProvider = CustomStorageProvider;
160
- //# sourceMappingURL=CustomStorage.js.map
@@ -1 +0,0 @@
1
- {"version":3,"names":["_reactNative","require","_reactNativeBlobUtil","_interopRequireDefault","e","__esModule","default","tokenCache","Map","registryCache","CustomStorageProvider","getEndpoint","config","key","fallback","customServer","endpoints","getAccessToken","Error","baseUrl","username","password","cacheKey","now","Date","cached","get","expiresAt","token","loginPath","loginUrl","endsWith","res","ReactNativeBlobUtil","fetch","JSON","stringify","json","parse","data","accessTokenInfo","accessToken","set","error","message","fetchManifest","bundledPatches","channel","baseFolder","registryPath","regCacheKey","appVersion","cachedReg","normalizedBaseUrl","slice","genLinkPath","genLinkUrl","encodeURIComponent","linkRes","Authorization","linkJson","registryUrl","reqHeaders","etag","registryRes","respInfo","status","patches","headers","registry","appGroup","apps","flavorGroup","flavors","toLowerCase","length","matchedPatches","filter","p","platform","Platform","OS","console","warn","getUpdateFileUrl","filename","useMinio","remotePath","downloadPath","getAssetUrl","hash","ext","exports"],"sourceRoot":"../../../../src","sources":["ota/storage/CustomStorage.ts"],"mappings":"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","ignoreList":[]}