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.
- package/README.md +47 -52
- package/android/src/main/java/vn/reactnativebspatch/BsPatchModule.kt +15 -102
- package/android/src/main/java/vn/reactnativebspatch/OtaBundleResolver.kt +21 -1
- package/cpp/bsdiff/bspatch.c +1 -0
- package/ios/OtaBundleResolver.swift +16 -1
- package/ios/RNBsPatch.mm +61 -154
- package/lib/commonjs/NativeBsPatch.js.map +1 -1
- package/lib/commonjs/index.js +4 -4
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ota/OtaProvider.js +26 -22
- package/lib/commonjs/ota/OtaProvider.js.map +1 -1
- package/lib/commonjs/ota/otaAssetResolver.js +8 -2
- package/lib/commonjs/ota/otaAssetResolver.js.map +1 -1
- package/lib/commonjs/ota/otaCore.js +89 -141
- package/lib/commonjs/ota/otaCore.js.map +1 -1
- package/lib/commonjs/ota/otaLogger.js +0 -2
- package/lib/commonjs/ota/otaLogger.js.map +1 -1
- package/lib/commonjs/ota/otaMetadata.js +18 -7
- package/lib/commonjs/ota/otaMetadata.js.map +1 -1
- package/lib/commonjs/ota/storage/GitHubRawStorage.js +3 -1
- package/lib/commonjs/ota/storage/GitHubRawStorage.js.map +1 -1
- package/lib/commonjs/ota/types.js +1 -10
- package/lib/commonjs/ota/types.js.map +1 -1
- package/lib/module/NativeBsPatch.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/ota/OtaProvider.js +26 -22
- package/lib/module/ota/OtaProvider.js.map +1 -1
- package/lib/module/ota/otaAssetResolver.js +8 -2
- package/lib/module/ota/otaAssetResolver.js.map +1 -1
- package/lib/module/ota/otaCore.js +88 -139
- package/lib/module/ota/otaCore.js.map +1 -1
- package/lib/module/ota/otaLogger.js +0 -2
- package/lib/module/ota/otaLogger.js.map +1 -1
- package/lib/module/ota/otaMetadata.js +18 -7
- package/lib/module/ota/otaMetadata.js.map +1 -1
- package/lib/module/ota/storage/GitHubRawStorage.js +3 -1
- package/lib/module/ota/storage/GitHubRawStorage.js.map +1 -1
- package/lib/module/ota/types.js +0 -9
- package/lib/module/ota/types.js.map +1 -1
- package/lib/typescript/NativeBsPatch.d.ts +3 -15
- package/lib/typescript/NativeBsPatch.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +1 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/ota/OtaProvider.d.ts.map +1 -1
- package/lib/typescript/ota/otaAssetResolver.d.ts.map +1 -1
- package/lib/typescript/ota/otaCore.d.ts +5 -42
- package/lib/typescript/ota/otaCore.d.ts.map +1 -1
- package/lib/typescript/ota/otaLogger.d.ts +1 -3
- package/lib/typescript/ota/otaLogger.d.ts.map +1 -1
- package/lib/typescript/ota/otaMetadata.d.ts +2 -2
- package/lib/typescript/ota/otaMetadata.d.ts.map +1 -1
- package/lib/typescript/ota/types.d.ts +18 -54
- package/lib/typescript/ota/types.d.ts.map +1 -1
- package/package.json +3 -6
- package/react-native-bspatch.podspec +0 -1
- package/src/NativeBsPatch.ts +3 -16
- package/src/index.ts +1 -1
- package/src/ota/OtaProvider.tsx +24 -23
- package/src/ota/otaAssetResolver.ts +5 -2
- package/src/ota/otaCore.ts +118 -176
- package/src/ota/otaLogger.ts +0 -2
- package/src/ota/otaMetadata.ts +18 -8
- package/src/ota/storage/GitHubRawStorage.ts +1 -1
- package/src/ota/types.ts +18 -57
- package/tools/build_ota.sh +356 -177
- package/tools/setup_project.js +95 -11
- package/tools/update_registry.js +24 -28
- package/tools/{upload_ota_custom.sh → upload_ota.sh} +33 -114
- package/lib/commonjs/crypto-js.d.js +0 -2
- package/lib/commonjs/crypto-js.d.js.map +0 -1
- package/lib/commonjs/ota/storage/CustomStorage.js +0 -160
- package/lib/commonjs/ota/storage/CustomStorage.js.map +0 -1
- package/lib/commonjs/ota/storage/GitHubReleaseStorage.js +0 -99
- package/lib/commonjs/ota/storage/GitHubReleaseStorage.js.map +0 -1
- package/lib/module/crypto-js.d.js +0 -2
- package/lib/module/crypto-js.d.js.map +0 -1
- package/lib/module/ota/storage/CustomStorage.js +0 -154
- package/lib/module/ota/storage/CustomStorage.js.map +0 -1
- package/lib/module/ota/storage/GitHubReleaseStorage.js +0 -93
- package/lib/module/ota/storage/GitHubReleaseStorage.js.map +0 -1
- package/lib/typescript/ota/storage/CustomStorage.d.ts +0 -12
- package/lib/typescript/ota/storage/CustomStorage.d.ts.map +0 -1
- package/lib/typescript/ota/storage/GitHubReleaseStorage.d.ts +0 -20
- package/lib/typescript/ota/storage/GitHubReleaseStorage.d.ts.map +0 -1
- package/src/crypto-js.d.ts +0 -1
- package/src/ota/storage/CustomStorage.ts +0 -166
- package/src/ota/storage/GitHubReleaseStorage.ts +0 -103
- package/tools/bsdiff +0 -0
package/tools/setup_project.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
108
|
-
|
|
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.
|
|
281
|
-
console.log('
|
|
282
|
-
console.log('
|
|
283
|
-
console.log('
|
|
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();
|
package/tools/update_registry.js
CHANGED
|
@@ -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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (signingKey)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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 "===
|
|
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"
|
|
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
|
|
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://
|
|
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
|
-
|
|
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"
|
|
122
|
+
for FILE in "$OUTPUT_DIR"/*.{patch,hbc,json}; do
|
|
108
123
|
[[ -f "$FILE" ]] || continue
|
|
109
124
|
FNAME=$(basename "$FILE")
|
|
110
|
-
|
|
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
|
-
|
|
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 +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":[]}
|