Skip to content

Commit 27cf3b3

Browse files
authored
Skip extracting auxiliary files and improve extraction progress for disk images (#2569)
1 parent 444a537 commit 27cf3b3

File tree

6 files changed

+156
-36
lines changed

6 files changed

+156
-36
lines changed

Autoupdate/SUDiskImageUnarchiver.m

Lines changed: 109 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,18 @@
1515

1616
#include "AppKitPrevention.h"
1717

18+
@interface SUDiskImageUnarchiver () <NSFileManagerDelegate>
19+
@end
20+
1821
@implementation SUDiskImageUnarchiver
1922
{
2023
NSString *_archivePath;
2124
NSString *_decryptionPassword;
2225
NSString *_extractionDirectory;
26+
27+
SUUnarchiverNotifier *_notifier;
28+
double _currentExtractionProgress;
29+
double _fileProgressIncrement;
2330
}
2431

2532
+ (BOOL)canUnarchivePath:(NSString *)path
@@ -51,6 +58,25 @@ - (void)unarchiveWithCompletionBlock:(void (^)(NSError * _Nullable))completionBl
5158
});
5259
}
5360

61+
static NSUInteger fileCountForDirectory(NSFileManager *fileManager, NSString *itemPath)
62+
{
63+
NSUInteger fileCount = 0;
64+
NSDirectoryEnumerator *dirEnum = [fileManager enumeratorAtPath:itemPath];
65+
for (NSString * __unused currentFile in dirEnum) {
66+
fileCount++;
67+
}
68+
69+
return fileCount;
70+
}
71+
72+
- (BOOL)fileManager:(NSFileManager *)fileManager shouldCopyItemAtURL:(NSURL *)srcURL toURL:(NSURL *)dstURL
73+
{
74+
_currentExtractionProgress += _fileProgressIncrement;
75+
[_notifier notifyProgress:_currentExtractionProgress];
76+
77+
return YES;
78+
}
79+
5480
// Called on a non-main thread.
5581
- (void)extractDMGWithNotifier:(SUUnarchiverNotifier *)notifier SPU_OBJC_DIRECT
5682
{
@@ -62,13 +88,12 @@ - (void)extractDMGWithNotifier:(SUUnarchiverNotifier *)notifier SPU_OBJC_DIRECT
6288
NSFileManager *manager;
6389
NSError *error = nil;
6490
NSArray *contents = nil;
65-
do
66-
{
91+
do {
6792
NSString *uuidString = [[NSUUID UUID] UUIDString];
6893
mountPoint = [@"/Volumes" stringByAppendingPathComponent:uuidString];
69-
}
94+
}
7095
// Note: this check does not follow symbolic links, which is what we want
71-
while ([[NSURL fileURLWithPath:mountPoint] checkResourceIsReachableAndReturnError:NULL]);
96+
while ([[NSURL fileURLWithPath:mountPoint] checkResourceIsReachableAndReturnError:NULL]);
7297

7398
NSData *promptData = [NSData dataWithBytes:"yes\n" length:4];
7499

@@ -92,6 +117,11 @@ - (void)extractDMGWithNotifier:(SUUnarchiverNotifier *)notifier SPU_OBJC_DIRECT
92117
NSData *output = nil;
93118
NSInteger taskResult = -1;
94119

120+
NSURL *mountPointURL = [NSURL fileURLWithPath:mountPoint isDirectory:YES];
121+
NSURL *extractionDirectoryURL = [NSURL fileURLWithPath:_extractionDirectory isDirectory:YES];
122+
NSMutableArray<NSString *> *itemsToExtract = [NSMutableArray array];
123+
NSUInteger totalFileExtractionCount = 0;
124+
95125
{
96126
NSTask *task = [[NSTask alloc] init];
97127
task.launchPath = @"/usr/bin/hdiutil";
@@ -122,8 +152,6 @@ - (void)extractDMGWithNotifier:(SUUnarchiverNotifier *)notifier SPU_OBJC_DIRECT
122152
goto reportError;
123153
}
124154

125-
[notifier notifyProgress:0.125];
126-
127155
if (@available(macOS 10.15, *)) {
128156
if (![inputPipe.fileHandleForWriting writeData:promptData error:&error]) {
129157
goto reportError;
@@ -147,50 +175,102 @@ - (void)extractDMGWithNotifier:(SUUnarchiverNotifier *)notifier SPU_OBJC_DIRECT
147175

148176
taskResult = task.terminationStatus;
149177
}
150-
151-
[notifier notifyProgress:0.5];
152178

153-
if (taskResult != 0)
154-
{
179+
if (taskResult != 0) {
155180
NSString *resultStr = output ? [[NSString alloc] initWithData:output encoding:NSUTF8StringEncoding] : nil;
156181
SULog(SULogLevelError, @"hdiutil failed with code: %ld data: <<%@>>", (long)taskResult, resultStr);
157182
goto reportError;
158183
}
159184
mountedSuccessfully = YES;
160185

186+
// Mounting can take some time, so increment progress
187+
_currentExtractionProgress = 0.1;
188+
[notifier notifyProgress:_currentExtractionProgress];
189+
161190
// Now that we've mounted it, we need to copy out its contents.
162191
manager = [[NSFileManager alloc] init];
163192
contents = [manager contentsOfDirectoryAtPath:mountPoint error:&error];
164-
if (contents == nil)
165-
{
193+
if (contents == nil) {
166194
SULog(SULogLevelError, @"Couldn't enumerate contents of archive mounted at %@: %@", mountPoint, error);
167195
goto reportError;
168196
}
169-
170-
double itemsCopied = 0;
171-
double totalItems = (double)[contents count];
172-
173-
for (NSString *item in contents)
174-
{
175-
NSString *fromPath = [mountPoint stringByAppendingPathComponent:item];
176-
NSString *toPath = [_extractionDirectory stringByAppendingPathComponent:item];
197+
198+
// Sparkle can support installing pkg files, app bundles, and other bundle types for plug-ins
199+
// We must not filter any of those out
200+
for (NSString *item in contents) {
201+
NSURL *fromPathURL = [mountPointURL URLByAppendingPathComponent:item];
177202

178-
itemsCopied += 1.0;
179-
[notifier notifyProgress:0.5 + itemsCopied/(totalItems*2.0)];
203+
NSString *lastPathComponent = fromPathURL.lastPathComponent;
180204

181-
// We skip any files in the DMG which are not readable but include the item in the progress
182-
if (![manager isReadableFileAtPath:fromPath]) {
205+
// Ignore hidden files
206+
if ([lastPathComponent hasPrefix:@"."]) {
183207
continue;
184208
}
185-
186-
SULog(SULogLevelDefault, @"copyItemAtPath:%@ toPath:%@", fromPath, toPath);
187-
188-
if (![manager copyItemAtPath:fromPath toPath:toPath error:&error])
189-
{
209+
210+
// Ignore aliases
211+
NSNumber *aliasFlag = nil;
212+
if ([fromPathURL getResourceValue:&aliasFlag forKey:NSURLIsAliasFileKey error:NULL] && aliasFlag.boolValue) {
213+
continue;
214+
}
215+
216+
// Ignore symbolic links
217+
NSNumber *symbolicFlag = nil;
218+
if ([fromPathURL getResourceValue:&symbolicFlag forKey:NSURLIsSymbolicLinkKey error:NULL] && symbolicFlag.boolValue) {
219+
continue;
220+
}
221+
222+
// Ensure file is readable
223+
NSNumber *isReadableFlag = nil;
224+
if ([fromPathURL getResourceValue:&isReadableFlag forKey:NSURLIsReadableKey error:NULL] && !isReadableFlag.boolValue) {
225+
continue;
226+
}
227+
228+
NSNumber *isDirectoryFlag = nil;
229+
if (![fromPathURL getResourceValue:&isDirectoryFlag forKey:NSURLIsDirectoryKey error:NULL]) {
230+
continue;
231+
}
232+
233+
BOOL isDirectory = isDirectoryFlag.boolValue;
234+
NSString *pathExtension = fromPathURL.pathExtension;
235+
236+
if (isDirectory) {
237+
// Skip directory types that aren't bundles or regular directories
238+
if ([pathExtension isEqualToString:@"rtfd"]) {
239+
continue;
240+
}
241+
} else {
242+
// The only non-directory files we care about are (m)pkg files
243+
if (![pathExtension isEqualToString:@"pkg"] && ![pathExtension isEqualToString:@"mpkg"]) {
244+
continue;
245+
}
246+
}
247+
248+
if (isDirectory) {
249+
totalFileExtractionCount += fileCountForDirectory(manager, fromPathURL.path);
250+
} else {
251+
totalFileExtractionCount++;
252+
}
253+
254+
[itemsToExtract addObject:item];
255+
}
256+
257+
_fileProgressIncrement = (0.99 - _currentExtractionProgress) / totalFileExtractionCount;
258+
_notifier = notifier;
259+
260+
// Copy all items we want to extract and notify of progress
261+
manager.delegate = self;
262+
for (NSString *item in itemsToExtract) {
263+
NSURL *fromURL = [mountPointURL URLByAppendingPathComponent:item];
264+
NSURL *toURL = [extractionDirectoryURL URLByAppendingPathComponent:item];
265+
266+
if (![manager copyItemAtURL:fromURL toURL:toURL error:&error]) {
267+
SULog(SULogLevelError, @"Failed to copy '%@' to '%@' with error: %@", fromURL.path, toURL.path, error);
190268
goto reportError;
191269
}
192270
}
193271

272+
[notifier notifyProgress:1.0];
273+
194274
[notifier notifySuccess];
195275
goto finally;
196276

Sparkle.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@
223223
724BB3AA1D3347C2005D534A /* SUInstallerStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = 724BB3971D333832005D534A /* SUInstallerStatus.m */; };
224224
724BB3B71D35ABA8005D534A /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 61B5F8F609C4CEB300B25A18 /* Security.framework */; };
225225
724F76F91D6EAD0D00ECD062 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 525A278F133D6AE900FD8D70 /* Cocoa.framework */; };
226+
725453552C04DB3700362123 /* SparkleTestCodeSign_apfs_lzma_aux_files.dmg in Resources */ = {isa = PBXBuildFile; fileRef = 725453542C04DB3700362123 /* SparkleTestCodeSign_apfs_lzma_aux_files.dmg */; };
226227
725602D51C83551C00DAA70E /* SUApplicationInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 725602D31C83551C00DAA70E /* SUApplicationInfo.h */; };
227228
725602D61C83551C00DAA70E /* SUApplicationInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 725602D41C83551C00DAA70E /* SUApplicationInfo.m */; };
228229
725B3A82263FBF0C0041AB8E /* testappcast_minimumAutoupdateVersion.xml in Resources */ = {isa = PBXBuildFile; fileRef = 725B3A81263FBF0C0041AB8E /* testappcast_minimumAutoupdateVersion.xml */; };
@@ -411,6 +412,7 @@
411412
72E45CF81B640DAE005C701A /* SUUpdateSettingsWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 72E45CF61B640DAE005C701A /* SUUpdateSettingsWindowController.xib */; };
412413
72E45CFC1B641961005C701A /* sparkletestcast.xml in Resources */ = {isa = PBXBuildFile; fileRef = 72E45CFB1B641961005C701A /* sparkletestcast.xml */; };
413414
72E539121D68C3FA0092CE5E /* SPUDownloadData.m in Sources */ = {isa = PBXBuildFile; fileRef = 72F9EC431D5E9ED8004AC8B6 /* SPUDownloadData.m */; };
415+
72E6D9712C04DE1A005496E4 /* SparkleTestCodeSign_pkg.dmg in Resources */ = {isa = PBXBuildFile; fileRef = 72E6D9702C04DE19005496E4 /* SparkleTestCodeSign_pkg.dmg */; };
414416
72EB735F29BE981300FBCEE7 /* DevSignedApp.zip in Resources */ = {isa = PBXBuildFile; fileRef = 72EB735E29BE981300FBCEE7 /* DevSignedApp.zip */; };
415417
72EB736129BEB36100FBCEE7 /* DevSignedAppVersion2.zip in Resources */ = {isa = PBXBuildFile; fileRef = 72EB736029BEB36100FBCEE7 /* DevSignedAppVersion2.zip */; };
416418
72EB87EA1CB8798800C37F42 /* ShowInstallerProgress.m in Sources */ = {isa = PBXBuildFile; fileRef = 72EB87E91CB8798800C37F42 /* ShowInstallerProgress.m */; };
@@ -1226,6 +1228,7 @@
12261228
724BB3A61D33461B005D534A /* SUXPCInstallerStatus.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUXPCInstallerStatus.h; sourceTree = "<group>"; };
12271229
724BB3A71D33461B005D534A /* SUXPCInstallerStatus.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUXPCInstallerStatus.m; sourceTree = "<group>"; };
12281230
724BB3B51D35AAC3005D534A /* ServiceManagement.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ServiceManagement.framework; path = System/Library/Frameworks/ServiceManagement.framework; sourceTree = SDKROOT; };
1231+
725453542C04DB3700362123 /* SparkleTestCodeSign_apfs_lzma_aux_files.dmg */ = {isa = PBXFileReference; lastKnownFileType = file; path = SparkleTestCodeSign_apfs_lzma_aux_files.dmg; sourceTree = "<group>"; };
12291232
725602D31C83551C00DAA70E /* SUApplicationInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SUApplicationInfo.h; sourceTree = "<group>"; };
12301233
725602D41C83551C00DAA70E /* SUApplicationInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUApplicationInfo.m; sourceTree = "<group>"; };
12311234
72563CA9272E1C5400AF39F0 /* .github */ = {isa = PBXFileReference; lastKnownFileType = folder; path = .github; sourceTree = "<group>"; };
@@ -1421,6 +1424,7 @@
14211424
72E45CF51B640DAE005C701A /* SUUpdateSettingsWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SUUpdateSettingsWindowController.m; sourceTree = "<group>"; };
14221425
72E45CF61B640DAE005C701A /* SUUpdateSettingsWindowController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SUUpdateSettingsWindowController.xib; sourceTree = "<group>"; };
14231426
72E45CFB1B641961005C701A /* sparkletestcast.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = sparkletestcast.xml; sourceTree = "<group>"; };
1427+
72E6D9702C04DE19005496E4 /* SparkleTestCodeSign_pkg.dmg */ = {isa = PBXFileReference; lastKnownFileType = file; path = SparkleTestCodeSign_pkg.dmg; sourceTree = "<group>"; };
14241428
72EB735E29BE981300FBCEE7 /* DevSignedApp.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = DevSignedApp.zip; sourceTree = "<group>"; };
14251429
72EB736029BEB36100FBCEE7 /* DevSignedAppVersion2.zip */ = {isa = PBXFileReference; lastKnownFileType = archive.zip; path = DevSignedAppVersion2.zip; sourceTree = "<group>"; };
14261430
72EB87E91CB8798800C37F42 /* ShowInstallerProgress.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ShowInstallerProgress.m; path = Sparkle/InstallerProgress/ShowInstallerProgress.m; sourceTree = SOURCE_ROOT; };
@@ -1855,6 +1859,8 @@
18551859
C23E88591BE7AF890050BB73 /* SparkleTestCodeSignApp.enc.dmg */,
18561860
72AC6B271B9AAD6700F62325 /* SparkleTestCodeSignApp.tar */,
18571861
72BC6C3C275027BF0083F14B /* SparkleTestCodeSign_apfs.dmg */,
1862+
725453542C04DB3700362123 /* SparkleTestCodeSign_apfs_lzma_aux_files.dmg */,
1863+
72E6D9702C04DE19005496E4 /* SparkleTestCodeSign_pkg.dmg */,
18581864
72AC6B291B9AAF3A00F62325 /* SparkleTestCodeSignApp.tar.bz2 */,
18591865
72AC6B251B9AAC8800F62325 /* SparkleTestCodeSignApp.tar.gz */,
18601866
72AC6B2B1B9AB0EE00F62325 /* SparkleTestCodeSignApp.tar.xz */,
@@ -3167,13 +3173,15 @@
31673173
72EB736129BEB36100FBCEE7 /* DevSignedAppVersion2.zip in Resources */,
31683174
723EDC3F26885A8E000BCBA4 /* testappcast_channels.xml in Resources */,
31693175
720DC50427A51A6500DFF3EC /* testappcast_minimumAutoupdateVersionSkipping.xml in Resources */,
3176+
72E6D9712C04DE1A005496E4 /* SparkleTestCodeSign_pkg.dmg in Resources */,
31703177
72AC6B2E1B9B218C00F62325 /* SparkleTestCodeSignApp.dmg in Resources */,
31713178
C23E885B1BE7B24F0050BB73 /* SparkleTestCodeSignApp.enc.dmg in Resources */,
31723179
72AC6B281B9AAD6700F62325 /* SparkleTestCodeSignApp.tar in Resources */,
31733180
72AC6B2A1B9AAF3A00F62325 /* SparkleTestCodeSignApp.tar.bz2 in Resources */,
31743181
72AC6B261B9AAC8800F62325 /* SparkleTestCodeSignApp.tar.gz in Resources */,
31753182
72AC6B2C1B9AB0EE00F62325 /* SparkleTestCodeSignApp.tar.xz in Resources */,
31763183
729F7ECE27409077004592DC /* SparkleTestCodeSignApp_bad_extraneous.zip in Resources */,
3184+
725453552C04DB3700362123 /* SparkleTestCodeSign_apfs_lzma_aux_files.dmg in Resources */,
31773185
5A5DD41D249F116E0045EB3E /* test-relative-urls.xml in Resources */,
31783186
F8761EB31ADC50EB000C9034 /* SparkleTestCodeSignApp.zip in Resources */,
31793187
5A5DD40424958B000045EB3E /* SUUpdateValidatorTest in Resources */,
Binary file not shown.
255 KB
Binary file not shown.

Tests/SUUnarchiverTest.swift

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,21 @@ class SUUnarchiverTest: XCTestCase
2424
let unarchivedSuccessExpectation = super.expectation(description: "Unarchived Success (format: \(archiveExtension))")
2525
let unarchivedFailureExpectation = super.expectation(description: "Unarchived Failure (format: \(archiveExtension))")
2626

27-
let extractedAppURL = tempDirectoryURL.appendingPathComponent(extractedAppName).appendingPathExtension("app")
28-
2927
self.unarchiveTestAppWithExtension(archiveExtension, appName: appName, tempDirectoryURL: tempDirectoryURL, archiveResourceURL: archiveResourceURL, password: password, expectingInstallationType: installationType, expectingSuccess: expectingSuccess, testExpectation: unarchivedSuccessExpectation)
3028
self.unarchiveNonExistentFileTestFailureAppWithExtension(archiveExtension, tempDirectoryURL: tempDirectoryURL, password: password, expectingInstallationType: installationType, testExpectation: unarchivedFailureExpectation)
3129

3230
super.waitForExpectations(timeout: 30.0, handler: nil)
3331

34-
if !archiveExtension.hasSuffix("pkg") && expectingSuccess {
35-
XCTAssertTrue(fileManager.fileExists(atPath: extractedAppURL.path))
36-
XCTAssertEqual("6a60ab31430cfca8fb499a884f4a29f73e59b472", hashOfTree(extractedAppURL.path))
32+
if expectingSuccess {
33+
if installationType == SPUInstallationTypeApplication {
34+
let extractedAppURL = tempDirectoryURL.appendingPathComponent(extractedAppName).appendingPathExtension("app")
35+
36+
XCTAssertTrue(fileManager.fileExists(atPath: extractedAppURL.path))
37+
XCTAssertEqual("6a60ab31430cfca8fb499a884f4a29f73e59b472", hashOfTree(extractedAppURL.path))
38+
} else if archiveExtension != "pkg" {
39+
let extractedPackageURL = tempDirectoryURL.appendingPathComponent(extractedAppName).appendingPathExtension("pkg")
40+
XCTAssertTrue(fileManager.fileExists(atPath: extractedPackageURL.path))
41+
}
3742
}
3843
}
3944

@@ -125,6 +130,16 @@ class SUUnarchiverTest: XCTestCase
125130
{
126131
self.unarchiveTestAppWithExtension("dmg", resourceName: "SparkleTestCodeSign_apfs")
127132
}
133+
134+
func testUnarchivingAPFSAdhocSignedDMGWithAuxFiles()
135+
{
136+
self.unarchiveTestAppWithExtension("dmg", resourceName: "SparkleTestCodeSign_apfs_lzma_aux_files")
137+
}
138+
139+
func testUnarchivingAPFSDMGWithPackage()
140+
{
141+
self.unarchiveTestAppWithExtension("dmg", resourceName: "SparkleTestCodeSign_pkg", expectingInstallationType: SPUInstallationTypeGuidedPackage)
142+
}
128143
#endif
129144

130145
#if SPARKLE_BUILD_PACKAGE_SUPPORT

sparkle-cli/SPUCommandLineUserDriver.m

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
@implementation SPUCommandLineUserDriver
1414
{
1515
SUUpdatePermissionResponse *_updatePermissionResponse;
16+
NSString *_lastProgressReported;
1617

1718
uint64_t _bytesDownloaded;
1819
uint64_t _bytesToDownload;
@@ -166,6 +167,7 @@ - (void)showUpdaterError:(NSError *)__unused error acknowledgement:(void (^)(voi
166167
- (void)showDownloadInitiatedWithCancellation:(void (^)(void))__unused cancellation
167168
{
168169
if (_verbose) {
170+
_lastProgressReported = nil;
169171
fprintf(stderr, "Downloading Update...\n");
170172
}
171173
}
@@ -189,21 +191,36 @@ - (void)showDownloadDidReceiveDataOfLength:(uint64_t)length
189191
}
190192

191193
if (_bytesToDownload > 0 && _verbose) {
192-
fprintf(stderr, "Downloaded %llu out of %llu bytes (%.0f%%)\n", _bytesDownloaded, _bytesToDownload, (_bytesDownloaded * 100.0 / _bytesToDownload));
194+
NSString *currentProgressPercentage = [NSString stringWithFormat:@"%.0f%%", (_bytesDownloaded * 100.0 / _bytesToDownload)];
195+
196+
// Only report progress advancement when percentage significantly advances
197+
if (_lastProgressReported == nil || ![_lastProgressReported isEqualToString:currentProgressPercentage]) {
198+
fprintf(stderr, "Downloaded %llu out of %llu bytes (%s)\n", _bytesDownloaded, _bytesToDownload, currentProgressPercentage.UTF8String);
199+
200+
_lastProgressReported = currentProgressPercentage;
201+
}
193202
}
194203
}
195204

196205
- (void)showDownloadDidStartExtractingUpdate
197206
{
198207
if (_verbose) {
208+
_lastProgressReported = nil;
199209
fprintf(stderr, "Extracting Update...\n");
200210
}
201211
}
202212

203213
- (void)showExtractionReceivedProgress:(double)progress
204214
{
205215
if (_verbose) {
206-
fprintf(stderr, "Extracting Update (%.0f%%)\n", progress * 100);
216+
NSString *currentProgressPercentage = [NSString stringWithFormat:@"%.0f%%", progress * 100];
217+
218+
// Only report progress advancement when percentage significantly advances
219+
if (_lastProgressReported == nil || ![_lastProgressReported isEqualToString:currentProgressPercentage]) {
220+
fprintf(stderr, "Extracting Update (%s)\n", currentProgressPercentage.UTF8String);
221+
222+
_lastProgressReported = currentProgressPercentage;
223+
}
207224
}
208225
}
209226

0 commit comments

Comments
 (0)