Swiftgram/Classes/BITPersistence.m
Christoph Wendt 56628bb824 Fix naming
2015-09-09 18:46:07 -07:00

302 lines
10 KiB
Objective-C

#import "HockeySDK.h"
#if HOCKEYSDK_FEATURE_TELEMETRY
#import "BITPersistence.h"
#import "BITPersistencePrivate.h"
#import "HockeySDKPrivate.h"
#import "BITHockeyHelper.h"
NSString *const kTelemetry = @"Telemetry";
NSString *const kMetaData = @"MetaData";
NSString *const kFileBaseString = @"hockey-app-bundle-";
NSString *const kFileBaseStringMeta = @"metadata";
NSString *const kTelemetryDirectoryPath = @"com.microsoft.HockeyApp/Telemetry/";
NSString *const kMetaDataDirectoryPath = @"com.microsoft.HockeyApp/MetaData/";
NSString *const BITPersistenceSuccessNotification = @"BITHockeyPersistenceSuccessNotification";
char const *kPersistenceQueueString = "com.microsoft.HockeyApp.persistenceQueue";
NSUInteger const defaultFileCount = 50;
@implementation BITPersistence {
BOOL _maxFileCountReached;
BOOL _directorySetupComplete;
}
#pragma mark - Public
- (instancetype)init {
self = [super init];
if(self) {
_persistenceQueue = dispatch_queue_create(kPersistenceQueueString, DISPATCH_QUEUE_SERIAL); //TODO several queues?
_requestedBundlePaths = [NSMutableArray new];
_maxFileCount = defaultFileCount;
// Evantually, there will be old files on disk, the flag will be updated before the first event gets created
_maxFileCountReached = YES;
_directorySetupComplete = NO; //will be set to true in createDirectoryStructureIfNeeded
[self createDirectoryStructureIfNeeded];
NSString *directoryPath = [self folderPathForType:BITPersistenceTypeTelemetry];
NSError *error = nil;
NSArray *fileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtURL:[NSURL fileURLWithPath:directoryPath]
includingPropertiesForKeys:@[NSURLNameKey]
options:NSDirectoryEnumerationSkipsHiddenFiles
error:&error];
_maxFileCountReached = fileNames.count >= _maxFileCount;
}
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) {
BITHockeyLog(@"Wrote bundle to %@", fileURL);
[strongSelf sendBundleSavedNotification];
}
else {
BITHockeyLog(@"Error writing bundle to %@", fileURL);
}
});
}
else {
BITHockeyLog(@"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 {
return !_maxFileCountReached;
}
- (NSString *)requestNextPath {
__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;
}
/**
* Deserializes a bundle from disk using NSKeyedUnarchiver
*
* @return a bundle of data or nil
*/
- (id)bundleAtPath:(NSString *)path {
id bundle = nil;
if(path && [path rangeOfString:kFileBaseString].location != NSNotFound) {
bundle = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
}
return bundle;
}
- (id)metaData {
NSString *path = [self fileURLForType:BITPersistenceTypeMetaData];
id bundle = nil;
if(path && [path rangeOfString:kFileBaseStringMeta].location != NSNotFound) {
bundle = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
}
return bundle;
}
- (NSData *)dataAtPath:(NSString *)path {
NSData *data = nil;
if(path && [path rangeOfString:kFileBaseString].location != NSNotFound) {
data = [NSData dataWithContentsOfFile:path];
}
return data;
}
/**
* Deletes a file at the given path.
*
* @param 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:kFileBaseString].location != NSNotFound) {
NSError *error = nil;
if(![[NSFileManager defaultManager] removeItemAtPath:path error:&error]) {
BITHockeyLog(@"Error deleting file at path %@", path);
}
else {
BITHockeyLog(@"Successfully deleted file at path %@", path);
[strongSelf.requestedBundlePaths removeObject:path];
}
} else {
BITHockeyLog(@"Empty path, nothing to delete");
}
});
}
- (void)giveBackRequestedPath:(NSString *)path {
__weak typeof(self) weakSelf = self;
dispatch_async(self.persistenceQueue, ^() {
typeof(self) strongSelf = weakSelf;
[strongSelf.requestedBundlePaths removeObject:path];
});
}
#pragma mark - Private
- (NSString *)fileURLForType:(BITPersistenceType)type {
NSString *appSupportPath = [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) lastObject];
NSString *fileName = nil;
NSString *filePath;
switch(type) {
case BITPersistenceTypeMetaData: {
fileName = kFileBaseStringMeta;
filePath = [appSupportPath stringByAppendingPathComponent:kMetaDataDirectoryPath];
break;
};
default: {
NSString *uuid = bit_UUID();
fileName = [NSString stringWithFormat:@"%@%@", kFileBaseString, uuid];
filePath = [appSupportPath stringByAppendingPathComponent:kTelemetryDirectoryPath];
break;
};
}
filePath = [filePath stringByAppendingPathComponent:fileName];
return filePath;
}
/**
* Create directory structure if necessary and exclude it from iCloud backup
*/
- (void)createDirectoryStructureIfNeeded {
//Application Support Dir
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *appSupportURL = [[fileManager URLsForDirectory:NSApplicationSupportDirectory inDomains:NSUserDomainMask] lastObject];
if(appSupportURL) {
NSError *error = nil;
//App Support and Telemetry Directory
NSURL *folderURL = [appSupportURL URLByAppendingPathComponent:kTelemetryDirectoryPath];
//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.
if(![fileManager createDirectoryAtURL:folderURL withIntermediateDirectories:YES attributes:nil error:&error]) {
BITHockeyLog(@"%@", error.localizedDescription);
return; //TODO we can't use persistence at all in this case, what do we want to do now? Notify the user?
}
//MetaData Directory
folderURL = [appSupportURL URLByAppendingPathComponent:kMetaDataDirectoryPath];
if(![fileManager createDirectoryAtURL:folderURL withIntermediateDirectories:NO attributes:nil error:&error]) {
BITHockeyLog(@"%@", error.localizedDescription);
return; //TODO we can't use persistence at all in this case, what do we want to do now? Notify the user?
}
_directorySetupComplete = YES;
//Exclude from Backup
if(![appSupportURL setResourceValue:@YES
forKey:NSURLIsExcludedFromBackupKey
error:&error]) {
BITHockeyLog(@"Error excluding %@ from backup %@", appSupportURL.lastPathComponent, error.localizedDescription);
}
else {
BITHockeyLog(@"Exclude %@ from backup", appSupportURL);
}
}
}
/**
* @returns the URL to the next file depending on the specified type. If there's no file, return nil.
*/
- (NSString *)nextURLOfType:(BITPersistenceType)type {
NSString *directoryPath = [self folderPathForType:type];
NSError *error = nil;
NSArray *fileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtURL:[NSURL fileURLWithPath:directoryPath]
includingPropertiesForKeys:@[NSURLNameKey]
options:NSDirectoryEnumerationSkipsHiddenFiles
error:&error];
// each track method asks, if space is still available. Getting the file count for each event would be too expensive,
// so let's get it here
if(type == BITPersistenceTypeTelemetry) {
_maxFileCountReached = fileNames.count >= _maxFileCount;
}
if(fileNames && fileNames.count > 0) {
for(NSURL *filename in fileNames) {
NSString *absolutePath = filename.path;
if(![self.requestedBundlePaths containsObject:absolutePath]) {
return absolutePath;
}
}
}
return nil;
}
- (NSString *)folderPathForType:(BITPersistenceType)type {
NSString *path = [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) lastObject];
NSString *subFolder = @"";
switch(type) {
case BITPersistenceTypeTelemetry: {
subFolder = kTelemetryDirectoryPath;
break;
}
case BITPersistenceTypeMetaData: {
subFolder = kMetaDataDirectoryPath;
break;
}
}
path = [path stringByAppendingPathComponent:subFolder];
return path;
}
/**
* 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(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:BITPersistenceSuccessNotification
object:nil
userInfo:nil];
});
}
@end
#endif /* HOCKEYSDK_FEATURE_TELEMETRY */