@capgo/capacitor-updater 6.14.26 → 6.14.33
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/CapgoCapacitorUpdater.podspec +3 -2
- package/Package.swift +2 -2
- package/README.md +350 -74
- package/android/build.gradle +20 -8
- package/android/proguard-rules.pro +22 -5
- package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +52 -16
- package/android/src/main/java/ee/forgr/capacitor_updater/Callback.java +2 -2
- package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +1202 -510
- package/android/src/main/java/ee/forgr/capacitor_updater/{CapacitorUpdater.java → CapgoUpdater.java} +566 -154
- package/android/src/main/java/ee/forgr/capacitor_updater/{CryptoCipher.java → CryptoCipherV1.java} +17 -9
- package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipherV2.java +15 -26
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +0 -3
- package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +260 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/DeviceIdHelper.java +221 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +300 -119
- package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +63 -25
- package/android/src/main/java/ee/forgr/capacitor_updater/Logger.java +338 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeDetector.java +72 -0
- package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +169 -0
- package/dist/docs.json +652 -63
- package/dist/esm/definitions.d.ts +274 -15
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/history.d.ts +1 -0
- package/dist/esm/history.js +283 -0
- package/dist/esm/history.js.map +1 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +12 -1
- package/dist/esm/web.js +29 -2
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +311 -2
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +311 -2
- package/dist/plugin.js.map +1 -1
- package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/AES.swift +6 -3
- package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +1578 -0
- package/ios/{Plugin/CapacitorUpdater.swift → Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift} +408 -139
- package/ios/{Plugin/CryptoCipher.swift → Sources/CapacitorUpdaterPlugin/CryptoCipherV1.swift} +13 -6
- package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/CryptoCipherV2.swift +33 -27
- package/ios/Sources/CapacitorUpdaterPlugin/DelayUpdateUtils.swift +220 -0
- package/ios/Sources/CapacitorUpdaterPlugin/DeviceIdHelper.swift +120 -0
- package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/InternalUtils.swift +47 -0
- package/ios/Sources/CapacitorUpdaterPlugin/Logger.swift +310 -0
- package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/RSA.swift +1 -0
- package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +112 -0
- package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/UserDefaultsExtension.swift +0 -2
- package/package.json +20 -16
- package/ios/Plugin/CapacitorUpdaterPlugin.swift +0 -1030
- /package/{LICENCE → LICENSE} +0 -0
- /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BigInt.swift +0 -0
- /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BundleInfo.swift +0 -0
- /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BundleStatus.swift +0 -0
- /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/DelayCondition.swift +0 -0
- /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/DelayUntilNext.swift +0 -0
- /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/Info.plist +0 -0
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
package ee.forgr.capacitor_updater;
|
|
7
7
|
|
|
8
8
|
import android.content.Context;
|
|
9
|
-
import android.util.Log;
|
|
10
9
|
import androidx.annotation.NonNull;
|
|
11
10
|
import androidx.work.Data;
|
|
12
11
|
import androidx.work.Worker;
|
|
@@ -17,6 +16,7 @@ import java.net.HttpURLConnection;
|
|
|
17
16
|
import java.net.URL;
|
|
18
17
|
import java.nio.channels.FileChannel;
|
|
19
18
|
import java.nio.file.Files;
|
|
19
|
+
import java.nio.file.StandardCopyOption;
|
|
20
20
|
import java.security.MessageDigest;
|
|
21
21
|
import java.util.ArrayList;
|
|
22
22
|
import java.util.Arrays;
|
|
@@ -28,18 +28,29 @@ import java.util.concurrent.Future;
|
|
|
28
28
|
import java.util.concurrent.TimeUnit;
|
|
29
29
|
import java.util.concurrent.atomic.AtomicBoolean;
|
|
30
30
|
import java.util.concurrent.atomic.AtomicLong;
|
|
31
|
+
import okhttp3.Interceptor;
|
|
31
32
|
import okhttp3.OkHttpClient;
|
|
32
33
|
import okhttp3.Protocol;
|
|
33
34
|
import okhttp3.Request;
|
|
34
35
|
import okhttp3.Response;
|
|
35
36
|
import okhttp3.ResponseBody;
|
|
37
|
+
import okio.Buffer;
|
|
38
|
+
import okio.BufferedSink;
|
|
39
|
+
import okio.BufferedSource;
|
|
40
|
+
import okio.Okio;
|
|
41
|
+
import okio.Source;
|
|
36
42
|
import org.brotli.dec.BrotliInputStream;
|
|
37
43
|
import org.json.JSONArray;
|
|
38
44
|
import org.json.JSONObject;
|
|
39
45
|
|
|
40
46
|
public class DownloadService extends Worker {
|
|
41
47
|
|
|
42
|
-
|
|
48
|
+
private static Logger logger;
|
|
49
|
+
|
|
50
|
+
public static void setLogger(Logger loggerInstance) {
|
|
51
|
+
logger = loggerInstance;
|
|
52
|
+
}
|
|
53
|
+
|
|
43
54
|
public static final String URL = "URL";
|
|
44
55
|
public static final String ID = "id";
|
|
45
56
|
public static final String PERCENT = "percent";
|
|
@@ -51,12 +62,51 @@ public class DownloadService extends Worker {
|
|
|
51
62
|
public static final String CHECKSUM = "checksum";
|
|
52
63
|
public static final String PUBLIC_KEY = "publickey";
|
|
53
64
|
public static final String IS_MANIFEST = "is_manifest";
|
|
65
|
+
public static final String APP_ID = "app_id";
|
|
66
|
+
public static final String PLUGIN_VERSION = "plugin_version";
|
|
54
67
|
private static final String UPDATE_FILE = "update.dat";
|
|
55
68
|
|
|
56
|
-
|
|
69
|
+
// Shared OkHttpClient to prevent resource leaks
|
|
70
|
+
protected static OkHttpClient sharedClient;
|
|
71
|
+
private static String currentAppId = "unknown";
|
|
72
|
+
private static String currentPluginVersion = "unknown";
|
|
73
|
+
private static String currentVersionOs = "unknown";
|
|
74
|
+
|
|
75
|
+
// Initialize shared client with User-Agent interceptor
|
|
76
|
+
static {
|
|
77
|
+
sharedClient = new OkHttpClient.Builder()
|
|
78
|
+
.protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1))
|
|
79
|
+
.addInterceptor((chain) -> {
|
|
80
|
+
Request originalRequest = chain.request();
|
|
81
|
+
String userAgent =
|
|
82
|
+
"CapacitorUpdater/" +
|
|
83
|
+
(currentPluginVersion != null ? currentPluginVersion : "unknown") +
|
|
84
|
+
" (" +
|
|
85
|
+
(currentAppId != null ? currentAppId : "unknown") +
|
|
86
|
+
") android/" +
|
|
87
|
+
(currentVersionOs != null ? currentVersionOs : "unknown");
|
|
88
|
+
Request requestWithUserAgent = originalRequest.newBuilder().header("User-Agent", userAgent).build();
|
|
89
|
+
return chain.proceed(requestWithUserAgent);
|
|
90
|
+
})
|
|
91
|
+
.build();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Method to update User-Agent values
|
|
95
|
+
public static void updateUserAgent(String appId, String pluginVersion, String versionOs) {
|
|
96
|
+
currentAppId = appId != null ? appId : "unknown";
|
|
97
|
+
currentPluginVersion = pluginVersion != null ? pluginVersion : "unknown";
|
|
98
|
+
currentVersionOs = versionOs != null ? versionOs : "unknown";
|
|
99
|
+
logger.debug(
|
|
100
|
+
"Updated User-Agent: CapacitorUpdater/" + currentPluginVersion + " (" + currentAppId + ") android/" + currentVersionOs
|
|
101
|
+
);
|
|
102
|
+
}
|
|
57
103
|
|
|
58
104
|
public DownloadService(@NonNull Context context, @NonNull WorkerParameters params) {
|
|
59
105
|
super(context, params);
|
|
106
|
+
// Use shared client - no need to create new instances
|
|
107
|
+
|
|
108
|
+
// Clean up old temporary files on service initialization
|
|
109
|
+
cleanupOldTempFiles(getApplicationContext().getCacheDir());
|
|
60
110
|
}
|
|
61
111
|
|
|
62
112
|
private void setProgress(int percent) {
|
|
@@ -94,7 +144,7 @@ public class DownloadService extends Worker {
|
|
|
94
144
|
String publicKey = getInputData().getString(PUBLIC_KEY);
|
|
95
145
|
boolean isManifest = getInputData().getBoolean(IS_MANIFEST, false);
|
|
96
146
|
|
|
97
|
-
|
|
147
|
+
logger.debug("doWork isManifest: " + isManifest);
|
|
98
148
|
|
|
99
149
|
if (isManifest) {
|
|
100
150
|
JSONArray manifest = DataManager.getInstance().getAndClearManifest();
|
|
@@ -102,7 +152,7 @@ public class DownloadService extends Worker {
|
|
|
102
152
|
handleManifestDownload(id, documentsDir, dest, version, sessionKey, publicKey, manifest.toString());
|
|
103
153
|
return createSuccessResult(dest, version, sessionKey, checksum, true);
|
|
104
154
|
} else {
|
|
105
|
-
|
|
155
|
+
logger.error("Manifest is null");
|
|
106
156
|
return createFailureResult("Manifest is null");
|
|
107
157
|
}
|
|
108
158
|
} else {
|
|
@@ -134,7 +184,7 @@ public class DownloadService extends Worker {
|
|
|
134
184
|
String manifestString
|
|
135
185
|
) {
|
|
136
186
|
try {
|
|
137
|
-
|
|
187
|
+
logger.debug("handleManifestDownload");
|
|
138
188
|
JSONArray manifest = new JSONArray(manifestString);
|
|
139
189
|
File destFolder = new File(documentsDir, dest);
|
|
140
190
|
File cacheFolder = new File(getApplicationContext().getCacheDir(), "capgo_downloads");
|
|
@@ -167,7 +217,7 @@ public class DownloadService extends Worker {
|
|
|
167
217
|
try {
|
|
168
218
|
fileHash = CryptoCipherV2.decryptChecksum(fileHash, publicKey);
|
|
169
219
|
} catch (Exception e) {
|
|
170
|
-
|
|
220
|
+
logger.error("Error decrypting checksum for " + fileName + "fileHash: " + fileHash);
|
|
171
221
|
hasError.set(true);
|
|
172
222
|
continue;
|
|
173
223
|
}
|
|
@@ -180,7 +230,7 @@ public class DownloadService extends Worker {
|
|
|
180
230
|
|
|
181
231
|
// Ensure parent directories of the target file exist
|
|
182
232
|
if (!Objects.requireNonNull(targetFile.getParentFile()).exists() && !targetFile.getParentFile().mkdirs()) {
|
|
183
|
-
|
|
233
|
+
logger.error("Failed to create parent directory for: " + targetFile.getAbsolutePath());
|
|
184
234
|
hasError.set(true);
|
|
185
235
|
continue;
|
|
186
236
|
}
|
|
@@ -189,10 +239,10 @@ public class DownloadService extends Worker {
|
|
|
189
239
|
try {
|
|
190
240
|
if (builtinFile.exists() && verifyChecksum(builtinFile, finalFileHash)) {
|
|
191
241
|
copyFile(builtinFile, targetFile);
|
|
192
|
-
|
|
242
|
+
logger.debug("using builtin file " + fileName);
|
|
193
243
|
} else if (cacheFile.exists() && verifyChecksum(cacheFile, finalFileHash)) {
|
|
194
244
|
copyFile(cacheFile, targetFile);
|
|
195
|
-
|
|
245
|
+
logger.debug("already cached " + fileName);
|
|
196
246
|
} else {
|
|
197
247
|
downloadAndVerify(downloadUrl, targetFile, cacheFile, finalFileHash, sessionKey, publicKey);
|
|
198
248
|
}
|
|
@@ -201,7 +251,7 @@ public class DownloadService extends Worker {
|
|
|
201
251
|
int percent = calcTotalPercent(completed, totalFiles);
|
|
202
252
|
setProgress(percent);
|
|
203
253
|
} catch (Exception e) {
|
|
204
|
-
|
|
254
|
+
logger.error("Error processing file: " + fileName + " " + e.getMessage());
|
|
205
255
|
hasError.set(true);
|
|
206
256
|
}
|
|
207
257
|
});
|
|
@@ -213,7 +263,7 @@ public class DownloadService extends Worker {
|
|
|
213
263
|
try {
|
|
214
264
|
future.get();
|
|
215
265
|
} catch (Exception e) {
|
|
216
|
-
|
|
266
|
+
logger.error("Error waiting for download " + e.getMessage());
|
|
217
267
|
hasError.set(true);
|
|
218
268
|
}
|
|
219
269
|
}
|
|
@@ -229,11 +279,11 @@ public class DownloadService extends Worker {
|
|
|
229
279
|
}
|
|
230
280
|
|
|
231
281
|
if (hasError.get()) {
|
|
232
|
-
|
|
282
|
+
logger.error("One or more files failed to download");
|
|
233
283
|
throw new IOException("One or more files failed to download");
|
|
234
284
|
}
|
|
235
285
|
} catch (Exception e) {
|
|
236
|
-
|
|
286
|
+
logger.error("Error in handleManifestDownload " + e.getMessage());
|
|
237
287
|
throw new RuntimeException(e.getLocalizedMessage());
|
|
238
288
|
}
|
|
239
289
|
}
|
|
@@ -248,93 +298,156 @@ public class DownloadService extends Worker {
|
|
|
248
298
|
String checksum
|
|
249
299
|
) {
|
|
250
300
|
File target = new File(documentsDir, dest);
|
|
251
|
-
File infoFile = new File(documentsDir, UPDATE_FILE);
|
|
252
|
-
|
|
253
|
-
|
|
301
|
+
File infoFile = new File(documentsDir, UPDATE_FILE);
|
|
302
|
+
File tempFile = new File(documentsDir, "temp" + ".tmp");
|
|
303
|
+
|
|
304
|
+
// Check available disk space before starting
|
|
305
|
+
long availableSpace = target.getParentFile().getUsableSpace();
|
|
306
|
+
long estimatedSize = 50 * 1024 * 1024; // 50MB default estimate
|
|
307
|
+
if (availableSpace < estimatedSize * 2) {
|
|
308
|
+
throw new RuntimeException("insufficient_disk_space");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
HttpURLConnection httpConn = null;
|
|
312
|
+
InputStream inputStream = null;
|
|
313
|
+
FileOutputStream outputStream = null;
|
|
314
|
+
BufferedReader reader = null;
|
|
315
|
+
BufferedWriter writer = null;
|
|
316
|
+
|
|
254
317
|
try {
|
|
255
318
|
URL u = new URL(url);
|
|
256
|
-
|
|
257
|
-
try {
|
|
258
|
-
httpConn = (HttpURLConnection) u.openConnection();
|
|
319
|
+
httpConn = (HttpURLConnection) u.openConnection();
|
|
259
320
|
|
|
260
|
-
|
|
261
|
-
|
|
321
|
+
// Set reasonable timeouts
|
|
322
|
+
httpConn.setConnectTimeout(30000); // 30 seconds
|
|
323
|
+
httpConn.setReadTimeout(60000); // 60 seconds
|
|
262
324
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
325
|
+
// Reading progress file (if exist)
|
|
326
|
+
long downloadedBytes = 0;
|
|
327
|
+
|
|
328
|
+
if (infoFile.exists() && tempFile.exists()) {
|
|
329
|
+
try {
|
|
330
|
+
reader = new BufferedReader(new FileReader(infoFile));
|
|
331
|
+
String updateVersion = reader.readLine();
|
|
332
|
+
if (updateVersion != null && !updateVersion.equals(version)) {
|
|
333
|
+
clearDownloadData(documentsDir);
|
|
334
|
+
} else {
|
|
335
|
+
downloadedBytes = tempFile.length();
|
|
336
|
+
}
|
|
337
|
+
} finally {
|
|
338
|
+
if (reader != null) {
|
|
339
|
+
try {
|
|
340
|
+
reader.close();
|
|
341
|
+
} catch (Exception ignored) {}
|
|
271
342
|
}
|
|
272
|
-
} else {
|
|
273
|
-
clearDownloadData(documentsDir);
|
|
274
343
|
}
|
|
344
|
+
} else {
|
|
345
|
+
clearDownloadData(documentsDir);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (downloadedBytes > 0) {
|
|
349
|
+
httpConn.setRequestProperty("Range", "bytes=" + downloadedBytes + "-");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
int responseCode = httpConn.getResponseCode();
|
|
353
|
+
|
|
354
|
+
if (responseCode == HttpURLConnection.HTTP_OK || responseCode == HttpURLConnection.HTTP_PARTIAL) {
|
|
355
|
+
long contentLength = httpConn.getContentLength() + downloadedBytes;
|
|
275
356
|
|
|
276
|
-
if
|
|
277
|
-
|
|
357
|
+
// Check if we have enough space for the actual file
|
|
358
|
+
if (contentLength > 0 && availableSpace < contentLength * 2) {
|
|
359
|
+
throw new RuntimeException("insufficient_disk_space");
|
|
278
360
|
}
|
|
279
361
|
|
|
280
|
-
|
|
362
|
+
try {
|
|
363
|
+
inputStream = httpConn.getInputStream();
|
|
364
|
+
outputStream = new FileOutputStream(tempFile, downloadedBytes > 0);
|
|
365
|
+
|
|
366
|
+
if (downloadedBytes == 0) {
|
|
367
|
+
writer = new BufferedWriter(new FileWriter(infoFile));
|
|
368
|
+
writer.write(String.valueOf(version));
|
|
369
|
+
writer.close();
|
|
370
|
+
writer = null;
|
|
371
|
+
}
|
|
281
372
|
|
|
282
|
-
|
|
283
|
-
|
|
373
|
+
byte[] buffer = new byte[8192]; // Larger buffer for better performance
|
|
374
|
+
int lastNotifiedPercent = 0;
|
|
375
|
+
int bytesRead;
|
|
284
376
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
// Updating the info file
|
|
295
|
-
try (BufferedWriter writer = new BufferedWriter(new FileWriter(infoFile))) {
|
|
296
|
-
writer.write(String.valueOf(version));
|
|
377
|
+
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
|
378
|
+
outputStream.write(buffer, 0, bytesRead);
|
|
379
|
+
downloadedBytes += bytesRead;
|
|
380
|
+
|
|
381
|
+
// Flush every 1MB to ensure progress is saved
|
|
382
|
+
if (downloadedBytes % (1024 * 1024) == 0) {
|
|
383
|
+
outputStream.flush();
|
|
297
384
|
}
|
|
298
385
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
downloadedBytes += bytesRead;
|
|
305
|
-
// Saving progress (flushing every 100 Ko)
|
|
306
|
-
if (downloadedBytes % 102400 == 0) {
|
|
307
|
-
outputStream.flush();
|
|
308
|
-
}
|
|
309
|
-
// Computing percentage
|
|
310
|
-
int percent = calcTotalPercent(downloadedBytes, contentLength);
|
|
311
|
-
while (lastNotifiedPercent + 10 <= percent) {
|
|
312
|
-
lastNotifiedPercent += 10;
|
|
313
|
-
// Artificial delay using CPU-bound calculation to take ~5 seconds
|
|
314
|
-
double result = 0;
|
|
315
|
-
setProgress(lastNotifiedPercent);
|
|
316
|
-
}
|
|
386
|
+
// Computing percentage
|
|
387
|
+
int percent = calcTotalPercent(downloadedBytes, contentLength);
|
|
388
|
+
if (percent >= lastNotifiedPercent + 10) {
|
|
389
|
+
lastNotifiedPercent = (percent / 10) * 10;
|
|
390
|
+
setProgress(lastNotifiedPercent);
|
|
317
391
|
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Final flush
|
|
395
|
+
outputStream.flush();
|
|
396
|
+
outputStream.close();
|
|
397
|
+
outputStream = null;
|
|
318
398
|
|
|
319
|
-
|
|
320
|
-
|
|
399
|
+
inputStream.close();
|
|
400
|
+
inputStream = null;
|
|
321
401
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
402
|
+
// Rename the temp file with the final name (dest)
|
|
403
|
+
if (!tempFile.renameTo(new File(documentsDir, dest))) {
|
|
404
|
+
throw new RuntimeException("Failed to rename temp file to final destination");
|
|
325
405
|
}
|
|
326
|
-
} else {
|
|
327
406
|
infoFile.delete();
|
|
407
|
+
} catch (OutOfMemoryError e) {
|
|
408
|
+
logger.error("Out of memory during download: " + e.getMessage());
|
|
409
|
+
// Try to free some memory
|
|
410
|
+
System.gc();
|
|
411
|
+
throw new RuntimeException("low_mem_fail");
|
|
412
|
+
} finally {
|
|
413
|
+
// Ensure all resources are closed
|
|
414
|
+
if (outputStream != null) {
|
|
415
|
+
try {
|
|
416
|
+
outputStream.close();
|
|
417
|
+
} catch (Exception ignored) {}
|
|
418
|
+
}
|
|
419
|
+
if (inputStream != null) {
|
|
420
|
+
try {
|
|
421
|
+
inputStream.close();
|
|
422
|
+
} catch (Exception ignored) {}
|
|
423
|
+
}
|
|
424
|
+
if (writer != null) {
|
|
425
|
+
try {
|
|
426
|
+
writer.close();
|
|
427
|
+
} catch (Exception ignored) {}
|
|
428
|
+
}
|
|
328
429
|
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
430
|
+
} else {
|
|
431
|
+
infoFile.delete();
|
|
432
|
+
throw new RuntimeException("HTTP error: " + responseCode);
|
|
333
433
|
}
|
|
334
434
|
} catch (OutOfMemoryError e) {
|
|
435
|
+
logger.error("Critical memory error: " + e.getMessage());
|
|
436
|
+
System.gc(); // Suggest garbage collection
|
|
335
437
|
throw new RuntimeException("low_mem_fail");
|
|
438
|
+
} catch (SecurityException e) {
|
|
439
|
+
logger.error("Security error during download: " + e.getMessage());
|
|
440
|
+
throw new RuntimeException("security_error: " + e.getMessage());
|
|
336
441
|
} catch (Exception e) {
|
|
337
|
-
|
|
442
|
+
logger.error("Download error: " + e.getMessage());
|
|
443
|
+
throw new RuntimeException(e.getMessage());
|
|
444
|
+
} finally {
|
|
445
|
+
// Ensure connection is closed
|
|
446
|
+
if (httpConn != null) {
|
|
447
|
+
try {
|
|
448
|
+
httpConn.disconnect();
|
|
449
|
+
} catch (Exception ignored) {}
|
|
450
|
+
}
|
|
338
451
|
}
|
|
339
452
|
}
|
|
340
453
|
|
|
@@ -347,7 +460,7 @@ public class DownloadService extends Worker {
|
|
|
347
460
|
infoFile.createNewFile();
|
|
348
461
|
tempFile.createNewFile();
|
|
349
462
|
} catch (IOException e) {
|
|
350
|
-
|
|
463
|
+
logger.error("Error in clearDownloadData " + e.getMessage());
|
|
351
464
|
// not a fatal error, so we don't throw an exception
|
|
352
465
|
}
|
|
353
466
|
}
|
|
@@ -373,62 +486,81 @@ public class DownloadService extends Worker {
|
|
|
373
486
|
String sessionKey,
|
|
374
487
|
String publicKey
|
|
375
488
|
) throws Exception {
|
|
376
|
-
|
|
489
|
+
logger.debug("downloadAndVerify " + downloadUrl);
|
|
377
490
|
|
|
378
491
|
Request request = new Request.Builder().url(downloadUrl).build();
|
|
379
492
|
|
|
493
|
+
// Check if file is a Brotli file
|
|
494
|
+
boolean isBrotli = targetFile.getName().endsWith(".br");
|
|
495
|
+
|
|
496
|
+
// Create final target file with .br extension removed if it's a Brotli file
|
|
497
|
+
File finalTargetFile = isBrotli
|
|
498
|
+
? new File(targetFile.getParentFile(), targetFile.getName().substring(0, targetFile.getName().length() - 3))
|
|
499
|
+
: targetFile;
|
|
500
|
+
|
|
380
501
|
// Create a temporary file for the compressed data
|
|
381
|
-
File compressedFile = new File(getApplicationContext().getCacheDir(), "temp_" + targetFile.getName() + ".
|
|
502
|
+
File compressedFile = new File(getApplicationContext().getCacheDir(), "temp_" + targetFile.getName() + ".tmp");
|
|
382
503
|
|
|
383
|
-
try (Response response =
|
|
504
|
+
try (Response response = sharedClient.newCall(request).execute()) {
|
|
384
505
|
if (!response.isSuccessful()) {
|
|
385
506
|
throw new IOException("Unexpected response code: " + response.code());
|
|
386
507
|
}
|
|
387
508
|
|
|
388
|
-
// Download compressed file
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
byte[] buffer = new byte[8192];
|
|
395
|
-
int bytesRead;
|
|
396
|
-
try (InputStream inputStream = responseBody.byteStream()) {
|
|
397
|
-
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
|
398
|
-
compressedFos.write(buffer, 0, bytesRead);
|
|
399
|
-
}
|
|
400
|
-
}
|
|
509
|
+
// Download compressed file atomically
|
|
510
|
+
ResponseBody responseBody = response.body();
|
|
511
|
+
if (responseBody == null) {
|
|
512
|
+
throw new IOException("Response body is null");
|
|
401
513
|
}
|
|
402
514
|
|
|
515
|
+
// Use OkIO for atomic write
|
|
516
|
+
writeFileAtomic(compressedFile, responseBody.byteStream(), null);
|
|
517
|
+
|
|
403
518
|
if (!publicKey.isEmpty() && sessionKey != null && !sessionKey.isEmpty()) {
|
|
404
|
-
|
|
519
|
+
logger.debug("Decrypting file " + targetFile.getName());
|
|
405
520
|
CryptoCipherV2.decryptFile(compressedFile, publicKey, sessionKey);
|
|
406
521
|
}
|
|
407
522
|
|
|
408
|
-
//
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
523
|
+
// Only decompress if file has .br extension
|
|
524
|
+
if (isBrotli) {
|
|
525
|
+
// Use new decompression method with atomic write
|
|
526
|
+
try (FileInputStream fis = new FileInputStream(compressedFile)) {
|
|
527
|
+
byte[] compressedData = new byte[(int) compressedFile.length()];
|
|
528
|
+
fis.read(compressedData);
|
|
529
|
+
byte[] decompressedData = decompressBrotli(compressedData, targetFile.getName());
|
|
530
|
+
|
|
531
|
+
// Write decompressed data atomically
|
|
532
|
+
try (java.io.ByteArrayInputStream bais = new java.io.ByteArrayInputStream(decompressedData)) {
|
|
533
|
+
writeFileAtomic(finalTargetFile, bais, null);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
} else {
|
|
537
|
+
// Just copy the file without decompression using atomic operation
|
|
538
|
+
try (FileInputStream fis = new FileInputStream(compressedFile)) {
|
|
539
|
+
writeFileAtomic(finalTargetFile, fis, null);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
412
542
|
|
|
413
543
|
// Delete the compressed file
|
|
414
544
|
compressedFile.delete();
|
|
415
|
-
String calculatedHash = CryptoCipherV2.calcChecksum(
|
|
545
|
+
String calculatedHash = CryptoCipherV2.calcChecksum(finalTargetFile);
|
|
416
546
|
|
|
417
547
|
// Verify checksum
|
|
418
548
|
if (calculatedHash.equals(expectedHash)) {
|
|
419
|
-
// Only cache if checksum is correct
|
|
420
|
-
|
|
549
|
+
// Only cache if checksum is correct - use atomic copy
|
|
550
|
+
try (FileInputStream fis = new FileInputStream(finalTargetFile)) {
|
|
551
|
+
writeFileAtomic(cacheFile, fis, expectedHash);
|
|
552
|
+
}
|
|
421
553
|
} else {
|
|
422
|
-
|
|
554
|
+
finalTargetFile.delete();
|
|
423
555
|
throw new IOException(
|
|
424
556
|
"Checksum verification failed for: " +
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
557
|
+
downloadUrl +
|
|
558
|
+
" " +
|
|
559
|
+
targetFile.getName() +
|
|
560
|
+
" expected: " +
|
|
561
|
+
expectedHash +
|
|
562
|
+
" calculated: " +
|
|
563
|
+
calculatedHash
|
|
432
564
|
);
|
|
433
565
|
}
|
|
434
566
|
} catch (Exception e) {
|
|
@@ -448,14 +580,14 @@ public class DownloadService extends Worker {
|
|
|
448
580
|
|
|
449
581
|
private String calculateFileHash(File file) throws Exception {
|
|
450
582
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
|
451
|
-
FileInputStream fis = new FileInputStream(file);
|
|
452
583
|
byte[] byteArray = new byte[1024];
|
|
453
584
|
int bytesCount = 0;
|
|
454
585
|
|
|
455
|
-
|
|
456
|
-
|
|
586
|
+
try (FileInputStream fis = new FileInputStream(file)) {
|
|
587
|
+
while ((bytesCount = fis.read(byteArray)) != -1) {
|
|
588
|
+
digest.update(byteArray, 0, bytesCount);
|
|
589
|
+
}
|
|
457
590
|
}
|
|
458
|
-
fis.close();
|
|
459
591
|
|
|
460
592
|
byte[] bytes = digest.digest();
|
|
461
593
|
StringBuilder sb = new StringBuilder();
|
|
@@ -468,7 +600,7 @@ public class DownloadService extends Worker {
|
|
|
468
600
|
private byte[] decompressBrotli(byte[] data, String fileName) throws IOException {
|
|
469
601
|
// Validate input
|
|
470
602
|
if (data == null) {
|
|
471
|
-
|
|
603
|
+
logger.error("Error: Null data received for " + fileName);
|
|
472
604
|
throw new IOException("Null data received");
|
|
473
605
|
}
|
|
474
606
|
|
|
@@ -495,7 +627,7 @@ public class DownloadService extends Worker {
|
|
|
495
627
|
return Arrays.copyOfRange(data, 3, data.length - 1);
|
|
496
628
|
}
|
|
497
629
|
} catch (ArrayIndexOutOfBoundsException e) {
|
|
498
|
-
|
|
630
|
+
logger.error("Error: Malformed data for " + fileName);
|
|
499
631
|
throw new IOException("Malformed data structure");
|
|
500
632
|
}
|
|
501
633
|
}
|
|
@@ -513,14 +645,63 @@ public class DownloadService extends Worker {
|
|
|
513
645
|
}
|
|
514
646
|
return bos.toByteArray();
|
|
515
647
|
} catch (IOException e) {
|
|
516
|
-
|
|
648
|
+
logger.error("Error: Brotli process failed for " + fileName + ". Status: " + e.getMessage());
|
|
517
649
|
// Add hex dump for debugging
|
|
518
650
|
StringBuilder hexDump = new StringBuilder();
|
|
519
651
|
for (int i = 0; i < Math.min(32, data.length); i++) {
|
|
520
652
|
hexDump.append(String.format("%02x ", data[i]));
|
|
521
653
|
}
|
|
522
|
-
|
|
654
|
+
logger.error("Error: Raw data (" + fileName + "): " + hexDump.toString());
|
|
523
655
|
throw e;
|
|
524
656
|
}
|
|
525
657
|
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Atomically write data to a file using OkIO
|
|
661
|
+
*/
|
|
662
|
+
private void writeFileAtomic(File targetFile, InputStream inputStream, String expectedChecksum) throws IOException {
|
|
663
|
+
File tempFile = new File(targetFile.getParent(), targetFile.getName() + ".tmp");
|
|
664
|
+
|
|
665
|
+
try {
|
|
666
|
+
// Write to temp file first using OkIO
|
|
667
|
+
try (BufferedSink sink = Okio.buffer(Okio.sink(tempFile)); BufferedSource source = Okio.buffer(Okio.source(inputStream))) {
|
|
668
|
+
sink.writeAll(source);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Verify checksum if provided
|
|
672
|
+
if (expectedChecksum != null && !expectedChecksum.isEmpty()) {
|
|
673
|
+
String actualChecksum = CryptoCipherV2.calcChecksum(tempFile);
|
|
674
|
+
if (!expectedChecksum.equalsIgnoreCase(actualChecksum)) {
|
|
675
|
+
tempFile.delete();
|
|
676
|
+
throw new IOException("Checksum verification failed");
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Atomic rename (on same filesystem)
|
|
681
|
+
Files.move(tempFile.toPath(), targetFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
|
682
|
+
} catch (Exception e) {
|
|
683
|
+
// Clean up temp file on error
|
|
684
|
+
if (tempFile.exists()) {
|
|
685
|
+
tempFile.delete();
|
|
686
|
+
}
|
|
687
|
+
throw new IOException("Failed to write file atomically: " + e.getMessage(), e);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Clean up old temporary files
|
|
693
|
+
*/
|
|
694
|
+
private void cleanupOldTempFiles(File directory) {
|
|
695
|
+
if (directory == null || !directory.exists()) return;
|
|
696
|
+
|
|
697
|
+
File[] tempFiles = directory.listFiles((dir, name) -> name.endsWith(".tmp"));
|
|
698
|
+
if (tempFiles != null) {
|
|
699
|
+
long oneHourAgo = System.currentTimeMillis() - 3600000;
|
|
700
|
+
for (File tempFile : tempFiles) {
|
|
701
|
+
if (tempFile.lastModified() < oneHourAgo) {
|
|
702
|
+
tempFile.delete();
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
526
707
|
}
|