/platforms/ios/CordovaLib/Classes/Private/Plugins/CDVLocalStorage/CDVLocalStorage.m
Objective C | 487 lines | 336 code | 98 blank | 53 comment | 58 complexity | 983e546cf9cafce697d48345b8ba63ec MD5 | raw file
- /*
- Licensed to the Apache Software Foundation (ASF) under one
- or more contributor license agreements. See the NOTICE file
- distributed with this work for additional information
- regarding copyright ownership. The ASF licenses this file
- to you under the Apache License, Version 2.0 (the
- "License"); you may not use this file except in compliance
- with the License. You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
- Unless required by applicable law or agreed to in writing,
- software distributed under the License is distributed on an
- "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- KIND, either express or implied. See the License for the
- specific language governing permissions and limitations
- under the License.
- */
- #import "CDVLocalStorage.h"
- #import "CDV.h"
- @interface CDVLocalStorage ()
- @property (nonatomic, readwrite, strong) NSMutableArray* backupInfo; // array of CDVBackupInfo objects
- @property (nonatomic, readwrite, weak) id <UIWebViewDelegate> webviewDelegate;
- @end
- @implementation CDVLocalStorage
- @synthesize backupInfo, webviewDelegate;
- - (void)pluginInitialize
- {
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onResignActive)
- name:UIApplicationWillResignActiveNotification object:nil];
- BOOL cloudBackup = [@"cloud" isEqualToString : self.commandDelegate.settings[[@"BackupWebStorage" lowercaseString]]];
- self.backupInfo = [[self class] createBackupInfoWithCloudBackup:cloudBackup];
- }
- #pragma mark -
- #pragma mark Plugin interface methods
- + (NSMutableArray*)createBackupInfoWithTargetDir:(NSString*)targetDir backupDir:(NSString*)backupDir targetDirNests:(BOOL)targetDirNests backupDirNests:(BOOL)backupDirNests rename:(BOOL)rename
- {
- /*
- This "helper" does so much work and has so many options it would probably be clearer to refactor the whole thing.
- Basically, there are three database locations:
- 1. "Normal" dir -- LIB/<nested dires WebKit/LocalStorage etc>/<normal filenames>
- 2. "Caches" dir -- LIB/Caches/<normal filenames>
- 3. "Backup" dir -- DOC/Backups/<renamed filenames>
- And between these three, there are various migration paths, most of which only consider 2 of the 3, which is why this helper is based on 2 locations and has a notion of "direction".
- */
- NSMutableArray* backupInfo = [NSMutableArray arrayWithCapacity:3];
- NSString* original;
- NSString* backup;
- CDVBackupInfo* backupItem;
- // ////////// LOCALSTORAGE
- original = [targetDir stringByAppendingPathComponent:targetDirNests ? @"WebKit/LocalStorage/file__0.localstorage":@"file__0.localstorage"];
- backup = [backupDir stringByAppendingPathComponent:(backupDirNests ? @"WebKit/LocalStorage" : @"")];
- backup = [backup stringByAppendingPathComponent:(rename ? @"localstorage.appdata.db" : @"file__0.localstorage")];
- backupItem = [[CDVBackupInfo alloc] init];
- backupItem.backup = backup;
- backupItem.original = original;
- backupItem.label = @"localStorage database";
- [backupInfo addObject:backupItem];
- // ////////// WEBSQL MAIN DB
- original = [targetDir stringByAppendingPathComponent:targetDirNests ? @"WebKit/LocalStorage/Databases.db":@"Databases.db"];
- backup = [backupDir stringByAppendingPathComponent:(backupDirNests ? @"WebKit/LocalStorage" : @"")];
- backup = [backup stringByAppendingPathComponent:(rename ? @"websqlmain.appdata.db" : @"Databases.db")];
- backupItem = [[CDVBackupInfo alloc] init];
- backupItem.backup = backup;
- backupItem.original = original;
- backupItem.label = @"websql main database";
- [backupInfo addObject:backupItem];
- // ////////// WEBSQL DATABASES
- original = [targetDir stringByAppendingPathComponent:targetDirNests ? @"WebKit/LocalStorage/file__0":@"file__0"];
- backup = [backupDir stringByAppendingPathComponent:(backupDirNests ? @"WebKit/LocalStorage" : @"")];
- backup = [backup stringByAppendingPathComponent:(rename ? @"websqldbs.appdata.db" : @"file__0")];
- backupItem = [[CDVBackupInfo alloc] init];
- backupItem.backup = backup;
- backupItem.original = original;
- backupItem.label = @"websql databases";
- [backupInfo addObject:backupItem];
- return backupInfo;
- }
- + (NSMutableArray*)createBackupInfoWithCloudBackup:(BOOL)cloudBackup
- {
- // create backup info from backup folder to caches folder
- NSString* appLibraryFolder = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0];
- NSString* appDocumentsFolder = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
- NSString* cacheFolder = [appLibraryFolder stringByAppendingPathComponent:@"Caches"];
- NSString* backupsFolder = [appDocumentsFolder stringByAppendingPathComponent:@"Backups"];
- // create the backups folder, if needed
- [[NSFileManager defaultManager] createDirectoryAtPath:backupsFolder withIntermediateDirectories:YES attributes:nil error:nil];
- [self addSkipBackupAttributeToItemAtURL:[NSURL fileURLWithPath:backupsFolder] skip:!cloudBackup];
- return [self createBackupInfoWithTargetDir:cacheFolder backupDir:backupsFolder targetDirNests:NO backupDirNests:NO rename:YES];
- }
- + (BOOL)addSkipBackupAttributeToItemAtURL:(NSURL*)URL skip:(BOOL)skip
- {
- NSError* error = nil;
- BOOL success = [URL setResourceValue:[NSNumber numberWithBool:skip] forKey:NSURLIsExcludedFromBackupKey error:&error];
- if (!success) {
- NSLog(@"Error excluding %@ from backup %@", [URL lastPathComponent], error);
- }
- return success;
- }
- + (BOOL)copyFrom:(NSString*)src to:(NSString*)dest error:(NSError* __autoreleasing*)error
- {
- NSFileManager* fileManager = [NSFileManager defaultManager];
- if (![fileManager fileExistsAtPath:src]) {
- NSString* errorString = [NSString stringWithFormat:@"%@ file does not exist.", src];
- if (error != NULL) {
- (*error) = [NSError errorWithDomain:kCDVLocalStorageErrorDomain
- code:kCDVLocalStorageFileOperationError
- userInfo:[NSDictionary dictionaryWithObject:errorString
- forKey:NSLocalizedDescriptionKey]];
- }
- return NO;
- }
- // generate unique filepath in temp directory
- CFUUIDRef uuidRef = CFUUIDCreate(kCFAllocatorDefault);
- CFStringRef uuidString = CFUUIDCreateString(kCFAllocatorDefault, uuidRef);
- NSString* tempBackup = [[NSTemporaryDirectory() stringByAppendingPathComponent:(__bridge NSString*)uuidString] stringByAppendingPathExtension:@"bak"];
- CFRelease(uuidString);
- CFRelease(uuidRef);
- BOOL destExists = [fileManager fileExistsAtPath:dest];
- // backup the dest
- if (destExists && ![fileManager copyItemAtPath:dest toPath:tempBackup error:error]) {
- return NO;
- }
- // remove the dest
- if (destExists && ![fileManager removeItemAtPath:dest error:error]) {
- return NO;
- }
- // create path to dest
- if (!destExists && ![fileManager createDirectoryAtPath:[dest stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:error]) {
- return NO;
- }
- // copy src to dest
- if ([fileManager copyItemAtPath:src toPath:dest error:error]) {
- // success - cleanup - delete the backup to the dest
- if ([fileManager fileExistsAtPath:tempBackup]) {
- [fileManager removeItemAtPath:tempBackup error:error];
- }
- return YES;
- } else {
- // failure - we restore the temp backup file to dest
- [fileManager copyItemAtPath:tempBackup toPath:dest error:error];
- // cleanup - delete the backup to the dest
- if ([fileManager fileExistsAtPath:tempBackup]) {
- [fileManager removeItemAtPath:tempBackup error:error];
- }
- return NO;
- }
- }
- - (BOOL)shouldBackup
- {
- for (CDVBackupInfo* info in self.backupInfo) {
- if ([info shouldBackup]) {
- return YES;
- }
- }
- return NO;
- }
- - (BOOL)shouldRestore
- {
- for (CDVBackupInfo* info in self.backupInfo) {
- if ([info shouldRestore]) {
- return YES;
- }
- }
- return NO;
- }
- /* copy from webkitDbLocation to persistentDbLocation */
- - (void)backup:(CDVInvokedUrlCommand*)command
- {
- NSString* callbackId = command.callbackId;
- NSError* __autoreleasing error = nil;
- CDVPluginResult* result = nil;
- NSString* message = nil;
- for (CDVBackupInfo* info in self.backupInfo) {
- if ([info shouldBackup]) {
- [[self class] copyFrom:info.original to:info.backup error:&error];
- if (callbackId) {
- if (error == nil) {
- message = [NSString stringWithFormat:@"Backed up: %@", info.label];
- NSLog(@"%@", message);
- result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:message];
- [self.commandDelegate sendPluginResult:result callbackId:callbackId];
- } else {
- message = [NSString stringWithFormat:@"Error in CDVLocalStorage (%@) backup: %@", info.label, [error localizedDescription]];
- NSLog(@"%@", message);
- result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:message];
- [self.commandDelegate sendPluginResult:result callbackId:callbackId];
- }
- }
- }
- }
- }
- /* copy from persistentDbLocation to webkitDbLocation */
- - (void)restore:(CDVInvokedUrlCommand*)command
- {
- NSError* __autoreleasing error = nil;
- CDVPluginResult* result = nil;
- NSString* message = nil;
- for (CDVBackupInfo* info in self.backupInfo) {
- if ([info shouldRestore]) {
- [[self class] copyFrom:info.backup to:info.original error:&error];
- if (error == nil) {
- message = [NSString stringWithFormat:@"Restored: %@", info.label];
- NSLog(@"%@", message);
- result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:message];
- [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
- } else {
- message = [NSString stringWithFormat:@"Error in CDVLocalStorage (%@) restore: %@", info.label, [error localizedDescription]];
- NSLog(@"%@", message);
- result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:message];
- [self.commandDelegate sendPluginResult:result callbackId:command.callbackId];
- }
- }
- }
- }
- + (void)__fixupDatabaseLocationsWithBackupType:(NSString*)backupType
- {
- [self __verifyAndFixDatabaseLocations];
- [self __restoreLegacyDatabaseLocationsWithBackupType:backupType];
- }
- + (void)__verifyAndFixDatabaseLocations
- {
- NSBundle* mainBundle = [NSBundle mainBundle];
- NSString* bundlePath = [[mainBundle bundlePath] stringByDeletingLastPathComponent];
- NSString* bundleIdentifier = [[mainBundle infoDictionary] objectForKey:@"CFBundleIdentifier"];
- NSString* appPlistPath = [bundlePath stringByAppendingPathComponent:[NSString stringWithFormat:@"Library/Preferences/%@.plist", bundleIdentifier]];
- NSMutableDictionary* appPlistDict = [NSMutableDictionary dictionaryWithContentsOfFile:appPlistPath];
- BOOL modified = [[self class] __verifyAndFixDatabaseLocationsWithAppPlistDict:appPlistDict
- bundlePath:bundlePath
- fileManager:[NSFileManager defaultManager]];
- if (modified) {
- BOOL ok = [appPlistDict writeToFile:appPlistPath atomically:YES];
- [[NSUserDefaults standardUserDefaults] synchronize];
- NSLog(@"Fix applied for database locations?: %@", ok ? @"YES" : @"NO");
- }
- }
- + (BOOL)__verifyAndFixDatabaseLocationsWithAppPlistDict:(NSMutableDictionary*)appPlistDict
- bundlePath:(NSString*)bundlePath
- fileManager:(NSFileManager*)fileManager
- {
- NSString* libraryCaches = @"Library/Caches";
- NSString* libraryWebKit = @"Library/WebKit";
- NSArray* keysToCheck = [NSArray arrayWithObjects:
- @"WebKitLocalStorageDatabasePathPreferenceKey",
- @"WebDatabaseDirectory",
- nil];
- BOOL dirty = NO;
- for (NSString* key in keysToCheck) {
- NSString* value = [appPlistDict objectForKey:key];
- // verify key exists, and path is in app bundle, if not - fix
- if ((value != nil) && ![value hasPrefix:bundlePath]) {
- // the pathSuffix to use may be wrong - OTA upgrades from < 5.1 to 5.1 do keep the old path Library/WebKit,
- // while Xcode synced ones do change the storage location to Library/Caches
- NSString* newBundlePath = [bundlePath stringByAppendingPathComponent:libraryCaches];
- if (![fileManager fileExistsAtPath:newBundlePath]) {
- newBundlePath = [bundlePath stringByAppendingPathComponent:libraryWebKit];
- }
- [appPlistDict setValue:newBundlePath forKey:key];
- dirty = YES;
- }
- }
- return dirty;
- }
- + (void)__restoreLegacyDatabaseLocationsWithBackupType:(NSString*)backupType
- {
- // on iOS 6, if you toggle between cloud/local backup, you must move database locations. Default upgrade from iOS5.1 to iOS6 is like a toggle from local to cloud.
- NSString* appLibraryFolder = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0];
- NSString* appDocumentsFolder = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
- NSMutableArray* backupInfo = [NSMutableArray arrayWithCapacity:0];
- if ([backupType isEqualToString:@"cloud"]) {
- #ifdef DEBUG
- NSLog(@"\n\nStarted backup to iCloud! Please be careful."
- "\nYour application might be rejected by Apple if you store too much data."
- "\nFor more information please read \"iOS Data Storage Guidelines\" at:"
- "\nhttps://developer.apple.com/icloud/documentation/data-storage/"
- "\nTo disable web storage backup to iCloud, set the BackupWebStorage preference to \"local\" in the Cordova config.xml file\n\n");
- #endif
- // We would like to restore old backups/caches databases to the new destination (nested in lib folder)
- [backupInfo addObjectsFromArray:[self createBackupInfoWithTargetDir:appLibraryFolder backupDir:[appDocumentsFolder stringByAppendingPathComponent:@"Backups"] targetDirNests:YES backupDirNests:NO rename:YES]];
- [backupInfo addObjectsFromArray:[self createBackupInfoWithTargetDir:appLibraryFolder backupDir:[appLibraryFolder stringByAppendingPathComponent:@"Caches"] targetDirNests:YES backupDirNests:NO rename:NO]];
- } else {
- // For ios6 local backups we also want to restore from Backups dir -- but we don't need to do that here, since the plugin will do that itself.
- [backupInfo addObjectsFromArray:[self createBackupInfoWithTargetDir:[appLibraryFolder stringByAppendingPathComponent:@"Caches"] backupDir:appLibraryFolder targetDirNests:NO backupDirNests:YES rename:NO]];
- }
- NSFileManager* manager = [NSFileManager defaultManager];
- for (CDVBackupInfo* info in backupInfo) {
- if ([manager fileExistsAtPath:info.backup]) {
- if ([info shouldRestore]) {
- NSLog(@"Restoring old webstorage backup. From: '%@' To: '%@'.", info.backup, info.original);
- [self copyFrom:info.backup to:info.original error:nil];
- }
- NSLog(@"Removing old webstorage backup: '%@'.", info.backup);
- [manager removeItemAtPath:info.backup error:nil];
- }
- }
- [[NSUserDefaults standardUserDefaults] setBool:[backupType isEqualToString:@"cloud"] forKey:@"WebKitStoreWebDataForBackup"];
- }
- #pragma mark -
- #pragma mark Notification handlers
- - (void)onResignActive
- {
- UIDevice* device = [UIDevice currentDevice];
- NSNumber* exitsOnSuspend = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIApplicationExitsOnSuspend"];
- BOOL isMultitaskingSupported = [device respondsToSelector:@selector(isMultitaskingSupported)] && [device isMultitaskingSupported];
- if (exitsOnSuspend == nil) { // if it's missing, it should be NO (i.e. multi-tasking on by default)
- exitsOnSuspend = [NSNumber numberWithBool:NO];
- }
- if (exitsOnSuspend) {
- [self backup:nil];
- } else if (isMultitaskingSupported) {
- __block UIBackgroundTaskIdentifier backgroundTaskID = UIBackgroundTaskInvalid;
- backgroundTaskID = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
- [[UIApplication sharedApplication] endBackgroundTask:backgroundTaskID];
- backgroundTaskID = UIBackgroundTaskInvalid;
- NSLog(@"Background task to backup WebSQL/LocalStorage expired.");
- }];
- CDVLocalStorage __weak* weakSelf = self;
- [self.commandDelegate runInBackground:^{
- [weakSelf backup:nil];
- [[UIApplication sharedApplication] endBackgroundTask:backgroundTaskID];
- backgroundTaskID = UIBackgroundTaskInvalid;
- }];
- }
- }
- - (void)onAppTerminate
- {
- [self onResignActive];
- }
- - (void)onReset
- {
- [self restore:nil];
- }
- @end
- #pragma mark -
- #pragma mark CDVBackupInfo implementation
- @implementation CDVBackupInfo
- @synthesize original, backup, label;
- - (BOOL)file:(NSString*)aPath isNewerThanFile:(NSString*)bPath
- {
- NSFileManager* fileManager = [NSFileManager defaultManager];
- NSError* __autoreleasing error = nil;
- NSDictionary* aPathAttribs = [fileManager attributesOfItemAtPath:aPath error:&error];
- NSDictionary* bPathAttribs = [fileManager attributesOfItemAtPath:bPath error:&error];
- NSDate* aPathModDate = [aPathAttribs objectForKey:NSFileModificationDate];
- NSDate* bPathModDate = [bPathAttribs objectForKey:NSFileModificationDate];
- if ((nil == aPathModDate) && (nil == bPathModDate)) {
- return NO;
- }
- return [aPathModDate compare:bPathModDate] == NSOrderedDescending || bPathModDate == nil;
- }
- - (BOOL)item:(NSString*)aPath isNewerThanItem:(NSString*)bPath
- {
- NSFileManager* fileManager = [NSFileManager defaultManager];
- BOOL aPathIsDir = NO, bPathIsDir = NO;
- BOOL aPathExists = [fileManager fileExistsAtPath:aPath isDirectory:&aPathIsDir];
- [fileManager fileExistsAtPath:bPath isDirectory:&bPathIsDir];
- if (!aPathExists) {
- return NO;
- }
- if (!(aPathIsDir && bPathIsDir)) { // just a file
- return [self file:aPath isNewerThanFile:bPath];
- }
- // essentially we want rsync here, but have to settle for our poor man's implementation
- // we get the files in aPath, and see if it is newer than the file in bPath
- // (it is newer if it doesn't exist in bPath) if we encounter the FIRST file that is newer,
- // we return YES
- NSDirectoryEnumerator* directoryEnumerator = [fileManager enumeratorAtPath:aPath];
- NSString* path;
- while ((path = [directoryEnumerator nextObject])) {
- NSString* aPathFile = [aPath stringByAppendingPathComponent:path];
- NSString* bPathFile = [bPath stringByAppendingPathComponent:path];
- BOOL isNewer = [self file:aPathFile isNewerThanFile:bPathFile];
- if (isNewer) {
- return YES;
- }
- }
- return NO;
- }
- - (BOOL)shouldBackup
- {
- return [self item:self.original isNewerThanItem:self.backup];
- }
- - (BOOL)shouldRestore
- {
- return [self item:self.backup isNewerThanItem:self.original];
- }
- @end