cordova-plugin-hot-updates 2.2.1 → 2.2.2
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 +12 -1
- package/package.json +1 -1
- package/plugin.xml +1 -1
- package/src/ios/HotUpdates.m +133 -154
package/README.md
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
|
-
# Cordova Hot Updates Plugin v2.2.
|
|
1
|
+
# Cordova Hot Updates Plugin v2.2.2
|
|
2
2
|
|
|
3
3
|
Frontend-controlled manual hot updates for Cordova iOS applications using WebView Reload approach.
|
|
4
4
|
|
|
5
|
+
## Changelog
|
|
6
|
+
|
|
7
|
+
### v2.2.2 (2024-11-28)
|
|
8
|
+
- **Fixed**: Failed download no longer corrupts previously downloaded update
|
|
9
|
+
- **Fixed**: Infinite loop when pending update installation fails (flags now cleared)
|
|
10
|
+
- **Fixed**: UserDefaults now saved only after successful download (not before)
|
|
11
|
+
- **Improved**: ZIP file validation with magic bytes check (PK\x03\x04) before extraction
|
|
12
|
+
- **Improved**: Cleaner logs without duplicates and full paths
|
|
13
|
+
- **Improved**: All errors now have consistent format with `ERROR:` prefix
|
|
14
|
+
- **Removed**: Redundant initialization code
|
|
15
|
+
|
|
5
16
|
[](https://badge.fury.io/js/cordova-plugin-hot-updates)
|
|
6
17
|
[](#license)
|
|
7
18
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cordova-plugin-hot-updates",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.2",
|
|
4
4
|
"description": "Frontend-controlled manual hot updates for Cordova iOS apps using WebView Reload approach. Manual updates only, JavaScript controls all decisions.",
|
|
5
5
|
"main": "www/HotUpdates.js",
|
|
6
6
|
"scripts": {
|
package/plugin.xml
CHANGED
package/src/ios/HotUpdates.m
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* - IgnoreList for tracking problematic versions
|
|
13
13
|
* - Auto-install pending updates on next app launch
|
|
14
14
|
*
|
|
15
|
-
* @version 2.
|
|
16
|
-
* @date 2025-11-
|
|
15
|
+
* @version 2.2.2
|
|
16
|
+
* @date 2025-11-28
|
|
17
17
|
* @author Mustafin Vladimir
|
|
18
18
|
* @copyright Copyright (c) 2025. All rights reserved.
|
|
19
19
|
*/
|
|
@@ -60,21 +60,14 @@ static BOOL hasPerformedInitialReload = NO;
|
|
|
60
60
|
isDownloadingUpdate = NO;
|
|
61
61
|
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:kDownloadInProgress];
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
pendingUpdateURL = nil;
|
|
65
|
-
pendingUpdateVersion = nil;
|
|
66
|
-
|
|
63
|
+
// Читаем состояние из UserDefaults
|
|
67
64
|
pendingUpdateURL = [[NSUserDefaults standardUserDefaults] stringForKey:kPendingUpdateURL];
|
|
68
65
|
isUpdateReadyToInstall = [[NSUserDefaults standardUserDefaults] boolForKey:kPendingUpdateReady];
|
|
69
66
|
if (isUpdateReadyToInstall) {
|
|
70
67
|
pendingUpdateVersion = [[NSUserDefaults standardUserDefaults] stringForKey:kPendingVersion];
|
|
71
|
-
NSLog(@"[HotUpdates] Found pending update ready to install: %@", pendingUpdateVersion);
|
|
72
68
|
}
|
|
73
69
|
|
|
74
|
-
NSLog(@"[HotUpdates]
|
|
75
|
-
NSLog(@"[HotUpdates] Bundle www path: %@", [[NSBundle mainBundle] pathForResource:kWWWDirName ofType:nil]);
|
|
76
|
-
NSLog(@"[HotUpdates] Documents www path: %@", wwwPath);
|
|
77
|
-
NSLog(@"[HotUpdates] Ignore list: %@", ignoreList);
|
|
70
|
+
NSLog(@"[HotUpdates] Initializing plugin...");
|
|
78
71
|
|
|
79
72
|
[self checkAndInstallPendingUpdate];
|
|
80
73
|
[self initializeWWWFolder];
|
|
@@ -85,15 +78,12 @@ static BOOL hasPerformedInitialReload = NO;
|
|
|
85
78
|
NSString *canaryVersion = [[NSUserDefaults standardUserDefaults] stringForKey:kCanaryVersion];
|
|
86
79
|
|
|
87
80
|
if (!canaryVersion || ![canaryVersion isEqualToString:currentVersion]) {
|
|
88
|
-
NSLog(@"[HotUpdates] Starting canary timer
|
|
89
|
-
|
|
81
|
+
NSLog(@"[HotUpdates] Starting canary timer for version %@", currentVersion);
|
|
90
82
|
[self startCanaryTimer];
|
|
91
|
-
} else {
|
|
92
|
-
NSLog(@"[HotUpdates] Canary already confirmed for version %@", currentVersion);
|
|
93
83
|
}
|
|
94
84
|
}
|
|
95
85
|
|
|
96
|
-
NSLog(@"[HotUpdates] Plugin initialized
|
|
86
|
+
NSLog(@"[HotUpdates] Plugin initialized (v%@)", appBundleVersion);
|
|
97
87
|
}
|
|
98
88
|
|
|
99
89
|
- (void)loadConfiguration {
|
|
@@ -101,9 +91,6 @@ static BOOL hasPerformedInitialReload = NO;
|
|
|
101
91
|
if (!appBundleVersion) {
|
|
102
92
|
appBundleVersion = @"1.0.0";
|
|
103
93
|
}
|
|
104
|
-
|
|
105
|
-
NSLog(@"[HotUpdates] Configuration loaded:");
|
|
106
|
-
NSLog(@" App bundle version: %@", appBundleVersion);
|
|
107
94
|
}
|
|
108
95
|
|
|
109
96
|
/*!
|
|
@@ -115,7 +102,7 @@ static BOOL hasPerformedInitialReload = NO;
|
|
|
115
102
|
NSString *pendingVersion = [[NSUserDefaults standardUserDefaults] stringForKey:kPendingVersion];
|
|
116
103
|
|
|
117
104
|
if (hasPendingUpdate && pendingVersion) {
|
|
118
|
-
NSLog(@"[HotUpdates]
|
|
105
|
+
NSLog(@"[HotUpdates] Auto-installing pending update: %@", pendingVersion);
|
|
119
106
|
|
|
120
107
|
[self backupCurrentVersion];
|
|
121
108
|
|
|
@@ -140,9 +127,15 @@ static BOOL hasPerformedInitialReload = NO;
|
|
|
140
127
|
|
|
141
128
|
[[NSFileManager defaultManager] removeItemAtPath:pendingUpdatePath error:nil];
|
|
142
129
|
|
|
143
|
-
NSLog(@"[HotUpdates] Update %@ installed successfully
|
|
130
|
+
NSLog(@"[HotUpdates] Update %@ installed successfully", pendingVersion);
|
|
144
131
|
} else {
|
|
145
|
-
NSLog(@"[HotUpdates] Failed to install update: %@", copyError.localizedDescription);
|
|
132
|
+
NSLog(@"[HotUpdates] Failed to install pending update: %@", copyError.localizedDescription);
|
|
133
|
+
// Очищаем флаги чтобы не пытаться снова при следующем запуске
|
|
134
|
+
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:kHasPending];
|
|
135
|
+
[[NSUserDefaults standardUserDefaults] removeObjectForKey:kPendingVersion];
|
|
136
|
+
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
137
|
+
// Удаляем битое обновление
|
|
138
|
+
[[NSFileManager defaultManager] removeItemAtPath:pendingUpdatePath error:nil];
|
|
146
139
|
}
|
|
147
140
|
}
|
|
148
141
|
}
|
|
@@ -156,7 +149,6 @@ static BOOL hasPerformedInitialReload = NO;
|
|
|
156
149
|
- (void)switchToUpdatedContentWithReload {
|
|
157
150
|
// Предотвращаем повторные перезагрузки при навигации между страницами
|
|
158
151
|
if (hasPerformedInitialReload) {
|
|
159
|
-
NSLog(@"[HotUpdates] Initial reload already performed, skipping");
|
|
160
152
|
return;
|
|
161
153
|
}
|
|
162
154
|
|
|
@@ -167,24 +159,19 @@ static BOOL hasPerformedInitialReload = NO;
|
|
|
167
159
|
NSString *indexPath = [documentsWwwPath stringByAppendingPathComponent:@"index.html"];
|
|
168
160
|
|
|
169
161
|
if ([[NSFileManager defaultManager] fileExistsAtPath:indexPath]) {
|
|
170
|
-
NSLog(@"[HotUpdates]
|
|
171
|
-
NSLog(@"[HotUpdates] Found installed update version: %@", installedVersion);
|
|
162
|
+
NSLog(@"[HotUpdates] Loading installed version: %@", installedVersion);
|
|
172
163
|
|
|
173
164
|
((CDVViewController *)self.viewController).wwwFolderName = documentsWwwPath;
|
|
174
|
-
NSLog(@"[HotUpdates] Changed wwwFolderName to: %@", documentsWwwPath);
|
|
175
|
-
|
|
176
165
|
hasPerformedInitialReload = YES;
|
|
177
166
|
|
|
178
167
|
// Очищаем кэш перед перезагрузкой, иначе может загрузиться старая версия
|
|
179
168
|
[self clearWebViewCacheWithCompletion:^{
|
|
180
169
|
[self reloadWebView];
|
|
181
|
-
NSLog(@"[HotUpdates] WebView reloaded with updated content (version: %@)", installedVersion);
|
|
182
170
|
}];
|
|
183
171
|
} else {
|
|
184
|
-
NSLog(@"[HotUpdates] Documents/www/index.html not found,
|
|
172
|
+
NSLog(@"[HotUpdates] WARNING: Documents/www/index.html not found, using bundle");
|
|
185
173
|
}
|
|
186
174
|
} else {
|
|
187
|
-
NSLog(@"[HotUpdates] No installed updates, using bundle www");
|
|
188
175
|
hasPerformedInitialReload = YES;
|
|
189
176
|
}
|
|
190
177
|
}
|
|
@@ -202,8 +189,6 @@ static BOOL hasPerformedInitialReload = NO;
|
|
|
202
189
|
* @param completion Block called after cache is cleared (on main thread)
|
|
203
190
|
*/
|
|
204
191
|
- (void)clearWebViewCacheWithCompletion:(void (^)(void))completion {
|
|
205
|
-
NSLog(@"[HotUpdates] Clearing WebView cache");
|
|
206
|
-
|
|
207
192
|
NSSet *websiteDataTypes = [NSSet setWithArray:@[
|
|
208
193
|
WKWebsiteDataTypeDiskCache,
|
|
209
194
|
WKWebsiteDataTypeMemoryCache,
|
|
@@ -215,7 +200,6 @@ static BOOL hasPerformedInitialReload = NO;
|
|
|
215
200
|
[[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:websiteDataTypes
|
|
216
201
|
modifiedSince:dateFrom
|
|
217
202
|
completionHandler:^{
|
|
218
|
-
NSLog(@"[HotUpdates] WebView cache cleared");
|
|
219
203
|
if (completion) {
|
|
220
204
|
dispatch_async(dispatch_get_main_queue(), completion);
|
|
221
205
|
}
|
|
@@ -231,26 +215,22 @@ static BOOL hasPerformedInitialReload = NO;
|
|
|
231
215
|
NSURL *fileURL = [NSURL fileURLWithPath:indexPath];
|
|
232
216
|
NSURL *allowReadAccessToURL = [NSURL fileURLWithPath:documentsWwwPath];
|
|
233
217
|
|
|
234
|
-
NSLog(@"[HotUpdates] Loading WebView with new URL: %@", fileURL.absoluteString);
|
|
235
|
-
|
|
236
218
|
id webViewEngine = cdvViewController.webViewEngine;
|
|
237
219
|
if (webViewEngine && [webViewEngine respondsToSelector:@selector(engineWebView)]) {
|
|
238
220
|
WKWebView *webView = [webViewEngine performSelector:@selector(engineWebView)];
|
|
239
221
|
|
|
240
222
|
if (webView && [webView isKindOfClass:[WKWebView class]]) {
|
|
241
|
-
// loadFileURL:allowingReadAccessToURL: правильно настраивает sandbox permissions для локальных файлов
|
|
242
223
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
243
224
|
[webView loadFileURL:fileURL allowingReadAccessToURL:allowReadAccessToURL];
|
|
244
|
-
NSLog(@"[HotUpdates] WebView loadFileURL executed with sandbox permissions");
|
|
245
225
|
});
|
|
246
226
|
} else {
|
|
247
|
-
NSLog(@"[HotUpdates] Could not access WKWebView for reload");
|
|
227
|
+
NSLog(@"[HotUpdates] ERROR: Could not access WKWebView for reload");
|
|
248
228
|
}
|
|
249
229
|
} else {
|
|
250
|
-
NSLog(@"[HotUpdates] WebView engine not available for reload");
|
|
230
|
+
NSLog(@"[HotUpdates] ERROR: WebView engine not available for reload");
|
|
251
231
|
}
|
|
252
232
|
} else {
|
|
253
|
-
NSLog(@"[HotUpdates] ViewController is not CDVViewController type");
|
|
233
|
+
NSLog(@"[HotUpdates] ERROR: ViewController is not CDVViewController type");
|
|
254
234
|
}
|
|
255
235
|
}
|
|
256
236
|
|
|
@@ -259,120 +239,126 @@ static BOOL hasPerformedInitialReload = NO;
|
|
|
259
239
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
260
240
|
|
|
261
241
|
if (![fileManager fileExistsAtPath:wwwPath]) {
|
|
262
|
-
NSLog(@"[HotUpdates] WWW folder not found in Documents. Creating and copying from bundle...");
|
|
263
|
-
|
|
264
242
|
NSString *bundleWWWPath = [[NSBundle mainBundle] pathForResource:@"www" ofType:nil];
|
|
265
243
|
if (bundleWWWPath) {
|
|
266
244
|
NSError *error;
|
|
267
245
|
[fileManager copyItemAtPath:bundleWWWPath toPath:wwwPath error:&error];
|
|
268
246
|
if (error) {
|
|
269
|
-
NSLog(@"[HotUpdates]
|
|
247
|
+
NSLog(@"[HotUpdates] ERROR: Failed to copy www folder: %@", error.localizedDescription);
|
|
270
248
|
} else {
|
|
271
|
-
NSLog(@"[HotUpdates]
|
|
249
|
+
NSLog(@"[HotUpdates] Initialized www folder from bundle");
|
|
272
250
|
}
|
|
273
251
|
} else {
|
|
274
|
-
NSLog(@"[HotUpdates]
|
|
252
|
+
NSLog(@"[HotUpdates] ERROR: Bundle www folder not found");
|
|
275
253
|
}
|
|
276
|
-
} else {
|
|
277
|
-
NSLog(@"[HotUpdates] WWW folder already exists in Documents");
|
|
278
254
|
}
|
|
279
255
|
}
|
|
280
256
|
|
|
281
257
|
- (BOOL)unzipFile:(NSString*)zipPath toDestination:(NSString*)destination {
|
|
282
|
-
NSLog(@"[HotUpdates] Unzipping %@ to %@", zipPath, destination);
|
|
283
|
-
|
|
284
258
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
285
259
|
NSError *error = nil;
|
|
286
260
|
|
|
261
|
+
// Проверяем существование ZIP файла
|
|
262
|
+
if (![fileManager fileExistsAtPath:zipPath]) {
|
|
263
|
+
NSLog(@"[HotUpdates] ERROR: ZIP file does not exist");
|
|
264
|
+
return NO;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Проверяем magic bytes (PK\x03\x04)
|
|
268
|
+
if (![self isValidZipFile:zipPath]) {
|
|
269
|
+
NSLog(@"[HotUpdates] ERROR: Invalid file format (not a ZIP archive)");
|
|
270
|
+
return NO;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
NSLog(@"[HotUpdates] Extracting update package...");
|
|
274
|
+
|
|
275
|
+
// Создаём директорию назначения если не существует
|
|
287
276
|
if (![fileManager fileExistsAtPath:destination]) {
|
|
288
277
|
[fileManager createDirectoryAtPath:destination withIntermediateDirectories:YES attributes:nil error:&error];
|
|
289
278
|
if (error) {
|
|
290
|
-
NSLog(@"[HotUpdates]
|
|
279
|
+
NSLog(@"[HotUpdates] ERROR: Failed to create destination directory: %@", error.localizedDescription);
|
|
291
280
|
return NO;
|
|
292
281
|
}
|
|
293
282
|
}
|
|
294
283
|
|
|
295
|
-
NSLog(@"[HotUpdates] Extracting ZIP archive using SSZipArchive library");
|
|
296
|
-
|
|
297
|
-
if (![[NSFileManager defaultManager] fileExistsAtPath:zipPath]) {
|
|
298
|
-
NSLog(@"[HotUpdates] ZIP file does not exist: %@", zipPath);
|
|
299
|
-
return NO;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
284
|
NSString *tempExtractPath = [destination stringByAppendingPathComponent:@"temp_extract"];
|
|
303
285
|
|
|
304
|
-
|
|
305
|
-
|
|
286
|
+
// Очищаем временную директорию
|
|
287
|
+
if ([fileManager fileExistsAtPath:tempExtractPath]) {
|
|
288
|
+
[fileManager removeItemAtPath:tempExtractPath error:nil];
|
|
306
289
|
}
|
|
307
290
|
|
|
308
|
-
if (![
|
|
309
|
-
NSLog(@"[HotUpdates] Failed to create temp
|
|
291
|
+
if (![fileManager createDirectoryAtPath:tempExtractPath withIntermediateDirectories:YES attributes:nil error:&error]) {
|
|
292
|
+
NSLog(@"[HotUpdates] ERROR: Failed to create temp directory: %@", error.localizedDescription);
|
|
310
293
|
return NO;
|
|
311
294
|
}
|
|
312
295
|
|
|
313
|
-
NSLog(@"[HotUpdates] Extracting to temp location: %@", tempExtractPath);
|
|
314
|
-
|
|
315
296
|
BOOL extractSuccess = [SSZipArchive unzipFileAtPath:zipPath toDestination:tempExtractPath];
|
|
316
297
|
|
|
317
|
-
if (extractSuccess) {
|
|
318
|
-
NSLog(@"[HotUpdates] ZIP
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
298
|
+
if (!extractSuccess) {
|
|
299
|
+
NSLog(@"[HotUpdates] ERROR: Failed to extract ZIP archive");
|
|
300
|
+
[fileManager removeItemAtPath:tempExtractPath error:nil];
|
|
301
|
+
return NO;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
NSArray *extractedContents = [fileManager contentsOfDirectoryAtPath:tempExtractPath error:nil];
|
|
305
|
+
|
|
306
|
+
// Ищем папку www (может быть вложенной)
|
|
307
|
+
NSString *wwwSourcePath = nil;
|
|
308
|
+
for (NSString *item in extractedContents) {
|
|
309
|
+
NSString *itemPath = [tempExtractPath stringByAppendingPathComponent:item];
|
|
310
|
+
BOOL isDirectory;
|
|
311
|
+
if ([fileManager fileExistsAtPath:itemPath isDirectory:&isDirectory] && isDirectory) {
|
|
312
|
+
if ([item isEqualToString:@"www"]) {
|
|
313
|
+
wwwSourcePath = itemPath;
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
NSString *nestedWwwPath = [itemPath stringByAppendingPathComponent:@"www"];
|
|
317
|
+
if ([fileManager fileExistsAtPath:nestedWwwPath]) {
|
|
318
|
+
wwwSourcePath = nestedWwwPath;
|
|
319
|
+
break;
|
|
338
320
|
}
|
|
339
321
|
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (!wwwSourcePath) {
|
|
325
|
+
NSLog(@"[HotUpdates] ERROR: www folder not found in ZIP archive");
|
|
326
|
+
[fileManager removeItemAtPath:tempExtractPath error:nil];
|
|
327
|
+
return NO;
|
|
328
|
+
}
|
|
340
329
|
|
|
341
|
-
|
|
342
|
-
NSLog(@"[HotUpdates] Found www folder at: %@", wwwSourcePath);
|
|
330
|
+
NSString *finalWwwPath = [destination stringByAppendingPathComponent:@"www"];
|
|
343
331
|
|
|
344
|
-
|
|
332
|
+
if ([fileManager fileExistsAtPath:finalWwwPath]) {
|
|
333
|
+
[fileManager removeItemAtPath:finalWwwPath error:nil];
|
|
334
|
+
}
|
|
345
335
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
336
|
+
NSError *copyError = nil;
|
|
337
|
+
BOOL copySuccess = [fileManager copyItemAtPath:wwwSourcePath toPath:finalWwwPath error:©Error];
|
|
349
338
|
|
|
350
|
-
|
|
351
|
-
BOOL copySuccess = [[NSFileManager defaultManager] copyItemAtPath:wwwSourcePath toPath:finalWwwPath error:©Error];
|
|
339
|
+
[fileManager removeItemAtPath:tempExtractPath error:nil];
|
|
352
340
|
|
|
353
|
-
|
|
354
|
-
|
|
341
|
+
if (!copySuccess) {
|
|
342
|
+
NSLog(@"[HotUpdates] ERROR: Failed to copy www folder: %@", copyError.localizedDescription);
|
|
343
|
+
return NO;
|
|
344
|
+
}
|
|
355
345
|
|
|
356
|
-
|
|
346
|
+
NSLog(@"[HotUpdates] Extraction completed successfully");
|
|
347
|
+
return YES;
|
|
348
|
+
}
|
|
357
349
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
NSLog(@"[HotUpdates] Error copying www folder: %@", copyError.localizedDescription);
|
|
362
|
-
}
|
|
363
|
-
} else {
|
|
364
|
-
NSLog(@"[HotUpdates] www folder not found in ZIP archive");
|
|
365
|
-
NSLog(@"[HotUpdates] Available contents: %@", extractedContents);
|
|
366
|
-
}
|
|
350
|
+
- (BOOL)isValidZipFile:(NSString*)filePath {
|
|
351
|
+
NSFileHandle *file = [NSFileHandle fileHandleForReadingAtPath:filePath];
|
|
352
|
+
if (!file) return NO;
|
|
367
353
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
NSLog(@"[HotUpdates] Failed to extract ZIP archive");
|
|
371
|
-
[[NSFileManager defaultManager] removeItemAtPath:tempExtractPath error:nil];
|
|
372
|
-
}
|
|
354
|
+
NSData *header = [file readDataOfLength:4];
|
|
355
|
+
[file closeFile];
|
|
373
356
|
|
|
374
|
-
|
|
375
|
-
|
|
357
|
+
if (header.length < 4) return NO;
|
|
358
|
+
|
|
359
|
+
const uint8_t *bytes = header.bytes;
|
|
360
|
+
// ZIP magic: PK\x03\x04 (0x504B0304)
|
|
361
|
+
return (bytes[0] == 0x50 && bytes[1] == 0x4B && bytes[2] == 0x03 && bytes[3] == 0x04);
|
|
376
362
|
}
|
|
377
363
|
|
|
378
364
|
#pragma mark - Settings Management
|
|
@@ -603,8 +589,7 @@ static BOOL hasPerformedInitialReload = NO;
|
|
|
603
589
|
updateVersion = @"pending";
|
|
604
590
|
}
|
|
605
591
|
|
|
606
|
-
NSLog(@"[HotUpdates] getUpdate
|
|
607
|
-
NSLog(@"[HotUpdates] Version: %@", updateVersion);
|
|
592
|
+
NSLog(@"[HotUpdates] getUpdate: v%@ from %@", updateVersion, downloadURL);
|
|
608
593
|
|
|
609
594
|
NSString *installedVersion = [[NSUserDefaults standardUserDefaults] stringForKey:kInstalledVersion];
|
|
610
595
|
if (installedVersion && [installedVersion isEqualToString:updateVersion]) {
|
|
@@ -632,11 +617,9 @@ static BOOL hasPerformedInitialReload = NO;
|
|
|
632
617
|
return;
|
|
633
618
|
}
|
|
634
619
|
|
|
620
|
+
// Сохраняем только в память, в UserDefaults запишем после успешной загрузки
|
|
635
621
|
pendingUpdateURL = downloadURL;
|
|
636
622
|
pendingUpdateVersion = updateVersion;
|
|
637
|
-
[[NSUserDefaults standardUserDefaults] setObject:downloadURL forKey:kPendingUpdateURL];
|
|
638
|
-
[[NSUserDefaults standardUserDefaults] setObject:updateVersion forKey:kPendingVersion];
|
|
639
|
-
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
640
623
|
|
|
641
624
|
[self downloadUpdateOnly:downloadURL callbackId:command.callbackId];
|
|
642
625
|
}
|
|
@@ -650,7 +633,7 @@ static BOOL hasPerformedInitialReload = NO;
|
|
|
650
633
|
|
|
651
634
|
NSURL *url = [NSURL URLWithString:downloadURL];
|
|
652
635
|
if (!url) {
|
|
653
|
-
NSLog(@"[HotUpdates] Invalid URL
|
|
636
|
+
NSLog(@"[HotUpdates] ERROR: Invalid URL format");
|
|
654
637
|
isDownloadingUpdate = NO;
|
|
655
638
|
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:kDownloadInProgress];
|
|
656
639
|
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
@@ -678,7 +661,7 @@ static BOOL hasPerformedInitialReload = NO;
|
|
|
678
661
|
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
679
662
|
|
|
680
663
|
if (error) {
|
|
681
|
-
NSLog(@"[HotUpdates] Download failed: %@", error.localizedDescription);
|
|
664
|
+
NSLog(@"[HotUpdates] ERROR: Download failed: %@", error.localizedDescription);
|
|
682
665
|
|
|
683
666
|
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
|
|
684
667
|
messageAsDictionary:[self createError:kErrorDownloadFailed
|
|
@@ -689,7 +672,7 @@ static BOOL hasPerformedInitialReload = NO;
|
|
|
689
672
|
|
|
690
673
|
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
|
|
691
674
|
if (httpResponse.statusCode != 200) {
|
|
692
|
-
NSLog(@"[HotUpdates]
|
|
675
|
+
NSLog(@"[HotUpdates] ERROR: HTTP %ld", (long)httpResponse.statusCode);
|
|
693
676
|
|
|
694
677
|
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
|
|
695
678
|
messageAsDictionary:[self createError:kErrorHTTPError
|
|
@@ -698,9 +681,7 @@ static BOOL hasPerformedInitialReload = NO;
|
|
|
698
681
|
return;
|
|
699
682
|
}
|
|
700
683
|
|
|
701
|
-
NSLog(@"[HotUpdates] Download completed
|
|
702
|
-
|
|
703
|
-
// Сохраняем скачанное обновление во временную папку
|
|
684
|
+
NSLog(@"[HotUpdates] Download completed, verifying...");
|
|
704
685
|
[self saveDownloadedUpdate:location callbackId:callbackId];
|
|
705
686
|
}];
|
|
706
687
|
|
|
@@ -708,24 +689,25 @@ static BOOL hasPerformedInitialReload = NO;
|
|
|
708
689
|
}
|
|
709
690
|
|
|
710
691
|
- (void)saveDownloadedUpdate:(NSURL*)updateLocation callbackId:(NSString*)callbackId {
|
|
711
|
-
NSLog(@"[HotUpdates] Saving downloaded update");
|
|
712
|
-
|
|
713
692
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
714
693
|
NSError *error;
|
|
715
694
|
|
|
695
|
+
// Используем временную папку для новой загрузки (не трогаем существующую temp_downloaded_update)
|
|
696
|
+
NSString *newDownloadPath = [documentsPath stringByAppendingPathComponent:@"temp_new_download"];
|
|
716
697
|
NSString *tempUpdatePath = [documentsPath stringByAppendingPathComponent:@"temp_downloaded_update"];
|
|
717
698
|
|
|
718
|
-
|
|
719
|
-
|
|
699
|
+
// Очищаем только папку для новой загрузки
|
|
700
|
+
if ([fileManager fileExistsAtPath:newDownloadPath]) {
|
|
701
|
+
[fileManager removeItemAtPath:newDownloadPath error:nil];
|
|
720
702
|
}
|
|
721
703
|
|
|
722
|
-
[fileManager createDirectoryAtPath:
|
|
704
|
+
[fileManager createDirectoryAtPath:newDownloadPath
|
|
723
705
|
withIntermediateDirectories:YES
|
|
724
706
|
attributes:nil
|
|
725
707
|
error:&error];
|
|
726
708
|
|
|
727
709
|
if (error) {
|
|
728
|
-
NSLog(@"[HotUpdates]
|
|
710
|
+
NSLog(@"[HotUpdates] ERROR: Failed to create temp directory: %@", error.localizedDescription);
|
|
729
711
|
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
|
|
730
712
|
messageAsDictionary:[self createError:kErrorTempDirError
|
|
731
713
|
message:@"Cannot create temp directory"]];
|
|
@@ -733,11 +715,10 @@ static BOOL hasPerformedInitialReload = NO;
|
|
|
733
715
|
return;
|
|
734
716
|
}
|
|
735
717
|
|
|
736
|
-
BOOL unzipSuccess = [self unzipFile:updateLocation.path toDestination:
|
|
718
|
+
BOOL unzipSuccess = [self unzipFile:updateLocation.path toDestination:newDownloadPath];
|
|
737
719
|
|
|
738
720
|
if (!unzipSuccess) {
|
|
739
|
-
|
|
740
|
-
[fileManager removeItemAtPath:tempUpdatePath error:nil];
|
|
721
|
+
[fileManager removeItemAtPath:newDownloadPath error:nil];
|
|
741
722
|
|
|
742
723
|
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
|
|
743
724
|
messageAsDictionary:[self createError:kErrorExtractionFailed
|
|
@@ -746,10 +727,10 @@ static BOOL hasPerformedInitialReload = NO;
|
|
|
746
727
|
return;
|
|
747
728
|
}
|
|
748
729
|
|
|
749
|
-
NSString *
|
|
750
|
-
if (![fileManager fileExistsAtPath:
|
|
751
|
-
NSLog(@"[HotUpdates] www folder not found in update package");
|
|
752
|
-
[fileManager removeItemAtPath:
|
|
730
|
+
NSString *newWwwPath = [newDownloadPath stringByAppendingPathComponent:kWWWDirName];
|
|
731
|
+
if (![fileManager fileExistsAtPath:newWwwPath]) {
|
|
732
|
+
NSLog(@"[HotUpdates] ERROR: www folder not found in update package");
|
|
733
|
+
[fileManager removeItemAtPath:newDownloadPath error:nil];
|
|
753
734
|
|
|
754
735
|
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
|
|
755
736
|
messageAsDictionary:[self createError:kErrorWWWNotFound
|
|
@@ -758,6 +739,12 @@ static BOOL hasPerformedInitialReload = NO;
|
|
|
758
739
|
return;
|
|
759
740
|
}
|
|
760
741
|
|
|
742
|
+
// Успех! Теперь безопасно заменяем старую temp_downloaded_update на новую
|
|
743
|
+
if ([fileManager fileExistsAtPath:tempUpdatePath]) {
|
|
744
|
+
[fileManager removeItemAtPath:tempUpdatePath error:nil];
|
|
745
|
+
}
|
|
746
|
+
[fileManager moveItemAtPath:newDownloadPath toPath:tempUpdatePath error:nil];
|
|
747
|
+
|
|
761
748
|
// Копируем в pending_update для автоустановки при следующем запуске
|
|
762
749
|
NSString *pendingPath = [documentsPath stringByAppendingPathComponent:kPendingUpdateDirName];
|
|
763
750
|
|
|
@@ -770,18 +757,18 @@ static BOOL hasPerformedInitialReload = NO;
|
|
|
770
757
|
error:&error];
|
|
771
758
|
|
|
772
759
|
if (!copySuccess) {
|
|
773
|
-
NSLog(@"[HotUpdates] Failed to copy to pending_update: %@", error);
|
|
774
|
-
} else {
|
|
775
|
-
NSLog(@"[HotUpdates] Copied to pending_update for auto-install on next launch");
|
|
760
|
+
NSLog(@"[HotUpdates] WARNING: Failed to copy to pending_update (auto-install disabled): %@", error.localizedDescription);
|
|
776
761
|
}
|
|
777
762
|
|
|
778
763
|
isUpdateReadyToInstall = YES;
|
|
764
|
+
// Сохраняем URL и версию в UserDefaults только после успешной загрузки
|
|
765
|
+
[[NSUserDefaults standardUserDefaults] setObject:pendingUpdateURL forKey:kPendingUpdateURL];
|
|
766
|
+
[[NSUserDefaults standardUserDefaults] setObject:pendingUpdateVersion forKey:kPendingVersion];
|
|
779
767
|
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:kPendingUpdateReady];
|
|
780
768
|
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:kHasPending];
|
|
781
769
|
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
782
770
|
|
|
783
|
-
NSLog(@"[HotUpdates] Update
|
|
784
|
-
NSLog(@"[HotUpdates] If user ignores popup, update will install automatically on next launch");
|
|
771
|
+
NSLog(@"[HotUpdates] Update ready (v%@)", pendingUpdateVersion);
|
|
785
772
|
|
|
786
773
|
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
|
|
787
774
|
[self.commandDelegate sendPluginResult:result callbackId:callbackId];
|
|
@@ -790,8 +777,6 @@ static BOOL hasPerformedInitialReload = NO;
|
|
|
790
777
|
#pragma mark - Force Update (Install Only)
|
|
791
778
|
|
|
792
779
|
- (void)forceUpdate:(CDVInvokedUrlCommand*)command {
|
|
793
|
-
NSLog(@"[HotUpdates] forceUpdate() called - installing downloaded update");
|
|
794
|
-
|
|
795
780
|
if (!isUpdateReadyToInstall) {
|
|
796
781
|
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
|
|
797
782
|
messageAsDictionary:[self createError:kErrorNoUpdateReady
|
|
@@ -804,7 +789,7 @@ static BOOL hasPerformedInitialReload = NO;
|
|
|
804
789
|
NSString *tempWwwPath = [tempUpdatePath stringByAppendingPathComponent:kWWWDirName];
|
|
805
790
|
|
|
806
791
|
if (![[NSFileManager defaultManager] fileExistsAtPath:tempWwwPath]) {
|
|
807
|
-
NSLog(@"[HotUpdates] Downloaded update files not found");
|
|
792
|
+
NSLog(@"[HotUpdates] ERROR: Downloaded update files not found");
|
|
808
793
|
|
|
809
794
|
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
|
|
810
795
|
messageAsDictionary:[self createError:kErrorUpdateFilesNotFound
|
|
@@ -817,13 +802,13 @@ static BOOL hasPerformedInitialReload = NO;
|
|
|
817
802
|
}
|
|
818
803
|
|
|
819
804
|
- (void)installDownloadedUpdate:(NSString*)tempWwwPath callbackId:(NSString*)callbackId {
|
|
820
|
-
NSLog(@"[HotUpdates] Installing update");
|
|
821
|
-
|
|
822
805
|
NSString *versionToInstall = [[NSUserDefaults standardUserDefaults] stringForKey:kPendingVersion];
|
|
823
806
|
if (!versionToInstall) {
|
|
824
807
|
versionToInstall = @"unknown";
|
|
825
808
|
}
|
|
826
809
|
|
|
810
|
+
NSLog(@"[HotUpdates] forceUpdate: installing v%@", versionToInstall);
|
|
811
|
+
|
|
827
812
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
|
828
813
|
NSError *error;
|
|
829
814
|
|
|
@@ -838,7 +823,7 @@ static BOOL hasPerformedInitialReload = NO;
|
|
|
838
823
|
error:&error];
|
|
839
824
|
|
|
840
825
|
if (!copySuccess) {
|
|
841
|
-
NSLog(@"[HotUpdates] Failed to install update: %@", error);
|
|
826
|
+
NSLog(@"[HotUpdates] ERROR: Failed to install update: %@", error.localizedDescription);
|
|
842
827
|
|
|
843
828
|
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR
|
|
844
829
|
messageAsDictionary:[self createError:kErrorInstallFailed
|
|
@@ -867,18 +852,14 @@ static BOOL hasPerformedInitialReload = NO;
|
|
|
867
852
|
pendingUpdateVersion = nil;
|
|
868
853
|
|
|
869
854
|
NSLog(@"[HotUpdates] Update installed successfully");
|
|
855
|
+
NSLog(@"[HotUpdates] Starting canary timer for version %@", newVersion);
|
|
870
856
|
|
|
871
857
|
CDVPluginResult* result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
|
|
872
858
|
[self.commandDelegate sendPluginResult:result callbackId:callbackId];
|
|
873
859
|
|
|
874
|
-
// После reloadWebView pluginInitialize НЕ вызывается, поэтому canary timer запускаем вручную
|
|
875
|
-
NSLog(@"[HotUpdates] Starting canary timer (20 seconds) for version %@", newVersion);
|
|
876
|
-
|
|
877
860
|
[self startCanaryTimer];
|
|
878
|
-
|
|
879
861
|
hasPerformedInitialReload = NO;
|
|
880
862
|
|
|
881
|
-
// Очищаем кэш WebView перед перезагрузкой, иначе может загрузиться старая версия
|
|
882
863
|
[self clearWebViewCacheWithCompletion:^{
|
|
883
864
|
[self reloadWebView];
|
|
884
865
|
}];
|
|
@@ -901,8 +882,6 @@ static BOOL hasPerformedInitialReload = NO;
|
|
|
901
882
|
return;
|
|
902
883
|
}
|
|
903
884
|
|
|
904
|
-
NSLog(@"[HotUpdates] Canary called for version: %@", canaryVersion);
|
|
905
|
-
|
|
906
885
|
// Сохраняем canary версию
|
|
907
886
|
[[NSUserDefaults standardUserDefaults] setObject:canaryVersion forKey:kCanaryVersion];
|
|
908
887
|
[[NSUserDefaults standardUserDefaults] synchronize];
|
|
@@ -911,7 +890,7 @@ static BOOL hasPerformedInitialReload = NO;
|
|
|
911
890
|
if (canaryTimer && [canaryTimer isValid]) {
|
|
912
891
|
[canaryTimer invalidate];
|
|
913
892
|
canaryTimer = nil;
|
|
914
|
-
NSLog(@"[HotUpdates] Canary
|
|
893
|
+
NSLog(@"[HotUpdates] Canary confirmed: v%@", canaryVersion);
|
|
915
894
|
}
|
|
916
895
|
|
|
917
896
|
// ТЗ: при успехе callback возвращает null
|