Peter 76e5a7fab6 Add 'submodules/HockeySDK-iOS/' from commit 'c7d0c7026303253e2ac576c02655691e5d314fe2'
git-subtree-dir: submodules/HockeySDK-iOS
git-subtree-mainline: 085acd26c4432939403765234266e3c1be0f3dd9
git-subtree-split: c7d0c7026303253e2ac576c02655691e5d314fe2
2019-06-11 18:53:14 +01:00

306 lines
10 KiB
Objective-C

#import "HockeySDK.h"
#if HOCKEYSDK_FEATURE_METRICS
#import "BITPersistence.h"
#import "BITPersistencePrivate.h"
#import "HockeySDKPrivate.h"
#import "BITHockeyHelper.h"
NSString *const BITPersistenceSuccessNotification = @"BITHockeyPersistenceSuccessNotification";
static NSString *const kBITTelemetry = @"Telemetry";
static NSString *const kBITMetaData = @"MetaData";
static NSString *const kBITFileBaseString = @"hockey-app-bundle-";
static NSString *const kBITFileBaseStringMeta = @"metadata";
static NSString *const kBITHockeyDirectory = @"com.microsoft.HockeyApp";
static NSString *const kBITTelemetryDirectory = @"Telemetry";
static NSString *const kBITMetaDataDirectory = @"MetaData";
static char const *kBITPersistenceQueueString = "com.microsoft.HockeyApp.persistenceQueue";
static NSUInteger const BITDefaultFileCount = 50;
@interface BITPersistence ()
@property (nonatomic) BOOL directorySetupComplete;
@end
@implementation BITPersistence
#pragma mark - Public
- (instancetype)init {
self = [super init];
if (self) {
_persistenceQueue = dispatch_queue_create(kBITPersistenceQueueString, DISPATCH_QUEUE_SERIAL); //TODO several queues?
_requestedBundlePaths = [NSMutableArray new];
_maxFileCount = BITDefaultFileCount;
// Evantually, there will be old files on disk, the flag will be updated before the first event gets created
_directorySetupComplete = NO; //will be set to true in createDirectoryStructureIfNeeded
[self createDirectoryStructureIfNeeded];
}
return self;
}
/**
* Saves the Bundle using NSKeyedArchiver and NSData's writeToFile:atomically
* Sends out a BITHockeyPersistenceSuccessNotification in case of success
*/
- (void)persistBundle:(NSData *)bundle {
//TODO send out a fail notification?
NSString *fileURL = [self fileURLForType:BITPersistenceTypeTelemetry];
if (bundle) {
__weak typeof(self) weakSelf = self;
dispatch_async(self.persistenceQueue, ^{
typeof(self) strongSelf = weakSelf;
BOOL success = [bundle writeToFile:fileURL atomically:YES];
if (success) {
BITHockeyLogDebug(@"INFO: Wrote bundle to %@", fileURL);
[strongSelf sendBundleSavedNotification];
}
else {
BITHockeyLogError(@"Error writing bundle to %@", fileURL);
}
});
}
else {
BITHockeyLogWarning(@"WARNING: Unable to write %@ as provided bundle was null", fileURL);
}
}
- (void)persistMetaData:(NSDictionary *)metaData {
NSString *fileURL = [self fileURLForType:BITPersistenceTypeMetaData];
//TODO send out a notification, too?!
dispatch_async(self.persistenceQueue, ^{
[NSKeyedArchiver archiveRootObject:metaData toFile:fileURL];
});
}
- (BOOL)isFreeSpaceAvailable {
NSArray *files = [self persistedFilesForType:BITPersistenceTypeTelemetry];
return files.count < self.maxFileCount;
}
- (NSString *)requestNextFilePath {
__block NSString *path = nil;
__weak typeof(self) weakSelf = self;
dispatch_sync(self.persistenceQueue, ^() {
typeof(self) strongSelf = weakSelf;
path = [strongSelf nextURLOfType:BITPersistenceTypeTelemetry];
if (path) {
[self.requestedBundlePaths addObject:path];
}
});
return path;
}
- (NSDictionary *)metaData {
NSString *filePath = [self fileURLForType:BITPersistenceTypeMetaData];
NSObject *bundle = [self bundleAtFilePath:filePath withFileBaseString:kBITFileBaseStringMeta];
if ([bundle isKindOfClass:NSDictionary.class]) {
return (NSDictionary *) bundle;
}
BITHockeyLogDebug(@"INFO: The context meta data file could not be loaded.");
return [NSDictionary dictionary];
}
- (NSObject *)bundleAtFilePath:(NSString *)filePath withFileBaseString:(NSString *)filebaseString {
id bundle = nil;
if (filePath && [filePath rangeOfString:filebaseString].location != NSNotFound) {
bundle = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
}
return bundle;
}
- (NSData *)dataAtFilePath:(NSString *)path {
NSData *data = nil;
if (path && [path rangeOfString:kBITFileBaseString].location != NSNotFound) {
data = [NSData dataWithContentsOfFile:path];
}
return data;
}
/**
* Deletes a file at the given path.
*
* @param path The path to look for a file and delete it.
*/
- (void)deleteFileAtPath:(NSString *)path {
__weak typeof(self) weakSelf = self;
dispatch_sync(self.persistenceQueue, ^() {
typeof(self) strongSelf = weakSelf;
if ([path rangeOfString:kBITFileBaseString].location != NSNotFound) {
NSError *error = nil;
if (![[NSFileManager defaultManager] removeItemAtPath:path error:&error]) {
BITHockeyLogError(@"Error deleting file at path %@", path);
}
else {
BITHockeyLogDebug(@"INFO: Successfully deleted file at path %@", path);
[strongSelf.requestedBundlePaths removeObject:path];
}
} else {
BITHockeyLogDebug(@"INFO: Empty path, nothing to delete");
}
});
}
- (void)giveBackRequestedFilePath:(NSString *)filePath {
__weak typeof(self) weakSelf = self;
dispatch_async(self.persistenceQueue, ^() {
typeof(self) strongSelf = weakSelf;
[strongSelf.requestedBundlePaths removeObject:filePath];
});
}
#pragma mark - Private
- (nullable NSString *)fileURLForType:(BITPersistenceType)type {
NSString *fileName = nil;
NSString *filePath;
switch (type) {
case BITPersistenceTypeMetaData: {
fileName = kBITFileBaseStringMeta;
filePath = [self.appHockeySDKDirectoryPath stringByAppendingPathComponent:kBITMetaDataDirectory];
break;
};
case BITPersistenceTypeTelemetry: {
NSString *uuid = bit_UUID();
fileName = [NSString stringWithFormat:@"%@%@", kBITFileBaseString, uuid];
filePath = [self.appHockeySDKDirectoryPath stringByAppendingPathComponent:kBITTelemetryDirectory];
break;
};
}
filePath = [filePath stringByAppendingPathComponent:fileName];
return filePath;
}
/**
* Create directory structure if necessary and exclude it from iCloud backup
*/
- (void)createDirectoryStructureIfNeeded {
// Using the local variable looks unnecessary but it actually silences a static analyzer warning.
NSString *appHockeySDKDirectoryPath = [self appHockeySDKDirectoryPath];
NSURL *appURL = [NSURL fileURLWithPath:appHockeySDKDirectoryPath];
NSFileManager *fileManager = [NSFileManager defaultManager];
if (appURL) {
NSError *error = nil;
// Create HockeySDK folder if needed
if (![fileManager createDirectoryAtURL:appURL withIntermediateDirectories:YES attributes:nil error:&error]) {
BITHockeyLogError(@"ERROR: %@", error.localizedDescription);
return;
}
// Create metadata subfolder
NSURL *metaDataURL = [appURL URLByAppendingPathComponent:kBITMetaDataDirectory];
if (![fileManager createDirectoryAtURL:metaDataURL withIntermediateDirectories:YES attributes:nil error:&error]) {
BITHockeyLogError(@"ERROR: %@", error.localizedDescription);
return;
}
// Create telemetry subfolder
//NOTE: createDirectoryAtURL:withIntermediateDirectories:attributes:error
//will return YES if the directory already exists and won't override anything.
//No need to check if the directory already exists.
NSURL *telemetryURL = [appURL URLByAppendingPathComponent:kBITTelemetryDirectory];
if (![fileManager createDirectoryAtURL:telemetryURL withIntermediateDirectories:YES attributes:nil error:&error]) {
BITHockeyLogError(@"ERROR: %@", error.localizedDescription);
return;
}
//Exclude HockeySDK folder from backup
if (![appURL setResourceValue:@YES
forKey:NSURLIsExcludedFromBackupKey
error:&error]) {
BITHockeyLogError(@"ERROR: Error excluding %@ from backup %@", appURL.lastPathComponent, error.localizedDescription);
} else {
BITHockeyLogDebug(@"INFO: Exclude %@ from backup", appURL);
}
self.directorySetupComplete = YES;
}
}
/**
* @returns the URL to the next file depending on the specified type. If there's no file, return nil.
*/
- (NSString *)nextURLOfType:(BITPersistenceType)type {
NSArray<NSURL *> *fileNames = [self persistedFilesForType:type];
if (fileNames && fileNames.count > 0) {
for (NSURL *filename in fileNames) {
NSString *absolutePath = filename.path;
if (![self.requestedBundlePaths containsObject:absolutePath]) {
return absolutePath;
}
}
}
return nil;
}
- (NSArray *)persistedFilesForType: (BITPersistenceType)type {
NSString *directoryPath = [self folderPathForType:type];
NSError *error = nil;
NSArray<NSURL *> *fileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtURL:[NSURL fileURLWithPath:directoryPath]
includingPropertiesForKeys:@[NSURLNameKey]
options:NSDirectoryEnumerationSkipsHiddenFiles
error:&error];
return fileNames;
}
- (NSString *)folderPathForType:(BITPersistenceType)type {
NSString *subFolder = @"";
switch (type) {
case BITPersistenceTypeTelemetry: {
subFolder = kBITTelemetryDirectory;
break;
}
case BITPersistenceTypeMetaData: {
subFolder = kBITMetaDataDirectory;
break;
}
}
return [self.appHockeySDKDirectoryPath stringByAppendingPathComponent:subFolder];
}
/**
* Send a BITHockeyPersistenceSuccessNotification to the main thread to notify observers that we have successfully saved a file
* This is typically used to trigger sending.
*/
- (void)sendBundleSavedNotification {
dispatch_async(dispatch_get_main_queue(), ^{
BITHockeyLogDebug(@"Sending notification: %@", BITPersistenceSuccessNotification);
[[NSNotificationCenter defaultCenter] postNotificationName:BITPersistenceSuccessNotification
object:nil
userInfo:nil];
});
}
- (NSString *)appHockeySDKDirectoryPath {
if (!_appHockeySDKDirectoryPath) {
NSString *appSupportPath = [[NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) lastObject] stringByStandardizingPath];
if (appSupportPath) {
_appHockeySDKDirectoryPath = [appSupportPath stringByAppendingPathComponent:kBITHockeyDirectory];
}
}
return _appHockeySDKDirectoryPath;
}
@end
#endif /* HOCKEYSDK_FEATURE_METRICS */