├── CHBgDropboxSync.h ├── CHBgDropboxSync.m ├── DropboxClearer.h ├── DropboxClearer.m └── readme.md /CHBgDropboxSync.h: -------------------------------------------------------------------------------- 1 | // 2 | // CHBgDropboxSync.h 3 | // Passwords 4 | // 5 | // Created by Chris Hulbert on 4/03/12. 6 | // 7 | 8 | #import 9 | #import "DBRestClient.h" 10 | 11 | @interface CHBgDropboxSync : NSObject 12 | 13 | + (void)start; 14 | + (void)forceStopIfRunning; 15 | + (void)clearLastSyncData; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /CHBgDropboxSync.m: -------------------------------------------------------------------------------- 1 | // 2 | // CHBgDropboxSync.m 3 | // Passwords 4 | // 5 | // Created by Chris Hulbert on 4/03/12. 6 | // 7 | // This uses ARC 8 | // Designed for DropboxSDK version 1.1 9 | 10 | #import "CHBgDropboxSync.h" 11 | #import 12 | #import "DropboxSDK.h" 13 | #import "ConciseKit.h" 14 | 15 | #define lastSyncDefaultsKey @"CHBgDropboxSyncLastSyncFiles" 16 | 17 | // Privates 18 | @interface CHBgDropboxSync() { 19 | UILabel* workingLabel; 20 | DBRestClient* client; 21 | BOOL anyLocalChanges; 22 | } 23 | - (NSDictionary*)getLocalStatus; 24 | @end 25 | 26 | // Singleton instance 27 | CHBgDropboxSync* bgDropboxSyncInstance=nil; 28 | 29 | @implementation CHBgDropboxSync 30 | 31 | #pragma mark - Showing and hiding the syncing indicator 32 | 33 | - (void)showWorking { 34 | if (workingLabel) return; // Already visible 35 | 36 | UIWindow* w = [[[UIApplication sharedApplication] delegate] window]; 37 | workingLabel = [[UILabel alloc] init]; 38 | workingLabel.textAlignment = UITextAlignmentRight; 39 | workingLabel.text = @"Syncing... "; 40 | workingLabel.textColor = [UIColor whiteColor]; 41 | int ht = 30; 42 | workingLabel.frame = CGRectMake(-120, 431-ht, 120, ht); 43 | workingLabel.backgroundColor = [UIColor colorWithWhite:0 alpha:0.5]; 44 | workingLabel.layer.cornerRadius = 10; 45 | 46 | // Spinner 47 | UIActivityIndicatorView *s = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite]; 48 | int gap = (workingLabel.frame.size.height - s.frame.size.height) / 2; 49 | s.frame = CGRectOffset(s.frame, 10+gap, gap); 50 | [s startAnimating]; 51 | [workingLabel addSubview:s]; 52 | 53 | // Swoosh it in 54 | [w addSubview:workingLabel]; 55 | [UIView animateWithDuration:0.3 animations:^{ 56 | workingLabel.frame = CGRectOffset(workingLabel.frame, 110, 0); 57 | }]; 58 | } 59 | 60 | - (void)hideWorking { 61 | if (!workingLabel) return; // Already hidden 62 | [UIView animateWithDuration:0.3 animations:^{ 63 | workingLabel.frame = CGRectMake(-workingLabel.frame.size.width, workingLabel.frame.origin.y, workingLabel.frame.size.width, workingLabel.frame.size.height); 64 | } completion:^(BOOL finished) { 65 | [workingLabel removeFromSuperview]; 66 | workingLabel = nil; 67 | }]; 68 | } 69 | 70 | #pragma mark - Startup 71 | 72 | - (void)startup { 73 | if (client) return; // Already started 74 | 75 | [self showWorking]; 76 | 77 | client = [[DBRestClient alloc] initWithSession:[DBSession sharedSession]]; 78 | client.delegate = self; 79 | 80 | // Start getting the remote file list 81 | [client loadMetadata:@"/"]; 82 | } 83 | 84 | #pragma mark - For keeping track of the last synced status of a file in the nsuserdefaults 85 | 86 | // This 'last sync status' is used to justify deletions - that is all it is used for. 87 | // Some thoughts on this 'last sync status' method of keeping track of deletions: 88 | // What happens if we update the 'last sync' for B after updating A? 89 | // Eg we overwrite the last sync state for B after we update A 90 | // Then we've lost track of whether we should do a deletion, and will start mistakenly doing downloads/uploads 91 | // Maybe only remove the last-sync status for each file one at a time as each file attempts deletion 92 | // And at sync completion, grab and update all of them one more time in case something ever slips through the net 93 | 94 | // Did the file exist locally at the end of the last sync? 95 | - (BOOL)lastSyncExists:(NSString*)file { 96 | return [[[NSUserDefaults standardUserDefaults] arrayForKey:lastSyncDefaultsKey] containsObject:file]; 97 | } 98 | 99 | // Clear all last sync data on pairing change 100 | - (void)lastSyncClear { 101 | [[NSUserDefaults standardUserDefaults] removeObjectForKey:lastSyncDefaultsKey]; 102 | [[NSUserDefaults standardUserDefaults] synchronize]; 103 | } 104 | 105 | // Do a full scan of the files and stores them all in the defaults. Only to be used when the sync is totally complete 106 | - (void)lastSyncCompletionRescan { 107 | [[NSUserDefaults standardUserDefaults] setObject:self.getLocalStatus.allKeys forKey:lastSyncDefaultsKey]; 108 | [[NSUserDefaults standardUserDefaults] synchronize]; 109 | } 110 | 111 | // Before you attempt to delete a file locally or remotely, call this so that it'll never try to delete that file again. 112 | // Do it before 'attempt delete' instead of 'confirm delete' just in case the delete fails, then we'll fall back to a 'download it again' state for the next sync, which is better than accidentally deleting it erroneously again later 113 | - (void)lastSyncRemove:(NSString*)file { 114 | NSMutableArray* arr = [NSMutableArray arrayWithArray:[[NSUserDefaults standardUserDefaults] arrayForKey:lastSyncDefaultsKey]]; 115 | [arr removeObject:file]; 116 | [[NSUserDefaults standardUserDefaults] setObject:arr forKey:lastSyncDefaultsKey]; 117 | [[NSUserDefaults standardUserDefaults] synchronize]; 118 | } 119 | 120 | #pragma mark - Completion / shutdown 121 | 122 | // Shutdown code that's common for success/fail/forced shutdowns 123 | - (void)internalCommonShutdown { 124 | // Autorelease the client using the following two lines, because we don't want it to release *just yet* because it probably called the function that called this, and would crash when the stack pops back to it. 125 | __autoreleasing DBRestClient* autoreleaseClient = client; 126 | [autoreleaseClient description]; 127 | 128 | // Now release the client 129 | client.delegate = nil; 130 | client = nil; 131 | 132 | // Free this singleton (put it on the autorelease pool, for safety's sake) 133 | __autoreleasing CHBgDropboxSync* autoreleaseSingleton = bgDropboxSyncInstance; 134 | [autoreleaseSingleton description]; 135 | bgDropboxSyncInstance = nil; // Clear the singleton 136 | 137 | if (anyLocalChanges) { // Only notify that there were changes at completion, not as we go, so the app doesn't get a half sync state 138 | [[NSNotificationCenter defaultCenter] postNotificationName:@"CHBgDropboxSyncUpdated" object:nil]; 139 | } 140 | } 141 | 142 | // For forced shutdowns eg closing the app 143 | - (void)internalShutdownForced { 144 | [self hideWorking]; 145 | [self internalCommonShutdown]; 146 | } 147 | 148 | // For clean shutdowns on sync success 149 | - (void)internalShutdownSuccess { 150 | [self lastSyncCompletionRescan]; 151 | workingLabel.text = @"Done "; 152 | [self performSelector:@selector(hideWorking) withObject:nil afterDelay:0.5]; 153 | [self internalCommonShutdown]; 154 | } 155 | 156 | // For failed shutdowns 157 | - (void)internalShutdownFailed { 158 | workingLabel.text = @"Failed "; 159 | [self performSelector:@selector(hideWorking) withObject:nil afterDelay:0.5]; 160 | [self internalCommonShutdown]; 161 | } 162 | 163 | #pragma mark - For when the steps complete 164 | 165 | // This re-starts the 'check the metadata' step again, which will then check for any syncing that needs doing, and then kick it off 166 | - (void)stepComplete { 167 | // Kick off the check the metadata with a little delay so we don't overdo things 168 | [client performSelector:@selector(loadMetadata:) withObject:@"/" afterDelay:.05]; 169 | } 170 | 171 | #pragma mark - The async dropbox steps 172 | 173 | - (void)startTaskLocalDelete:(NSString*)file { 174 | NSLog(@"Sync: Deleting local file %@", file); 175 | [[NSFileManager defaultManager] removeItemAtPath:[$.documentPath stringByAppendingPathComponent:file] error:nil]; 176 | [self stepComplete]; 177 | anyLocalChanges = YES; // So that when we complete, we notify that there were local changes 178 | } 179 | 180 | // Upload 181 | - (void)startTaskUpload:(NSString*)file rev:(NSString*)rev { 182 | NSLog(@"Sync: Uploading file %@, %@", file, rev?@"overwriting":@"new"); 183 | [client uploadFile:file toPath:@"/" withParentRev:rev fromPath:[$.documentPath stringByAppendingPathComponent:file]]; 184 | } 185 | - (void)restClient:(DBRestClient *)client uploadedFile:(NSString *)destPath from:(NSString *)srcPath metadata:(DBMetadata *)metadata { 186 | // Now the file has uploaded, we need to set its 'last modified' date locally to match the date on dropbox. 187 | // Unfortunately we can't change the dropbox date to match the local date, which would be more appropriate, really. 188 | NSDictionary* attr = $dict(metadata.lastModifiedDate, NSFileModificationDate); 189 | [[NSFileManager defaultManager] setAttributes:attr ofItemAtPath:srcPath error:nil]; 190 | [self stepComplete]; 191 | } 192 | - (void)restClient:(DBRestClient*)client uploadFileFailedWithError:(NSError*)error { 193 | [self internalShutdownFailed]; 194 | } 195 | // End upload 196 | 197 | // Download 198 | - (void)startTaskDownload:(NSString*)file { 199 | NSLog(@"Sync: Downloading file %@", file); 200 | [client loadFile:$str(@"/%@", file) intoPath:[$.documentPath stringByAppendingPathComponent:file]]; 201 | } 202 | - (void)restClient:(DBRestClient*)client loadedFile:(NSString*)destPath contentType:(NSString*)contentType metadata:(DBMetadata*)metadata { 203 | // Now the file has downloaded, we need to set its 'last modified' date locally to match the date on dropbox 204 | NSLog(@"Downloaded >%@<, it's DB date is: %@", destPath, [metadata.lastModifiedDate descriptionWithLocale:[NSLocale currentLocale]]); 205 | NSDictionary* attr = $dict(metadata.lastModifiedDate, NSFileModificationDate); 206 | [[NSFileManager defaultManager] setAttributes:attr ofItemAtPath:destPath error:nil]; 207 | [self stepComplete]; 208 | anyLocalChanges = YES; // So that when we complete, we notify that there were local changes 209 | } 210 | - (void)restClient:(DBRestClient *)client loadFileFailedWithError:(NSError *)error { 211 | [self internalShutdownFailed]; 212 | } 213 | // End download 214 | 215 | // Remote delete 216 | - (void)startTaskRemoteDelete:(NSString*)file { 217 | NSLog(@"Sync: Deleting remote file %@", file); 218 | [client deletePath:$str(@"/%@", file)]; 219 | [self stepComplete]; 220 | } 221 | - (void)restClient:(DBRestClient *)client deletedPath:(NSString *)path { 222 | [self stepComplete]; 223 | } 224 | - (void)restClient:(DBRestClient *)client deletePathFailedWithError:(NSError *)error { 225 | [self internalShutdownFailed]; 226 | } 227 | // End remote delete 228 | 229 | #pragma mark - Figure out what needs doing after we get the remote metadata 230 | 231 | // Get the current status of files and folders as a dict: Path (eg 'abc.txt') => last mod date 232 | - (NSDictionary*)getLocalStatus { 233 | NSMutableDictionary* localFiles = [NSMutableDictionary dictionary]; 234 | NSString* root = $.documentPath; // Where are we going to sync to 235 | for (NSString* item in [[NSFileManager defaultManager] contentsOfDirectoryAtPath:root error:nil]) { 236 | // Skip hidden/system files - you may want to change this if your files start with ., however dropbox errors on many 'ignored' files such as .DS_Store which you'll want to skip 237 | if ([item hasPrefix:@"."]) continue; 238 | 239 | // Get the full path and attribs 240 | NSString* itemPath = [root stringByAppendingPathComponent:item]; 241 | NSDictionary* attribs = [[NSFileManager defaultManager] attributesOfItemAtPath:itemPath error:nil]; 242 | BOOL isFile = $eql(attribs.fileType, NSFileTypeRegular); 243 | 244 | if (isFile) { 245 | [localFiles setObject:attribs.fileModificationDate forKey:item]; 246 | } 247 | } 248 | return localFiles; 249 | } 250 | 251 | // Starts a single sync step, returning YES if nothing needs doing. RemoteFiles is: Path (eg 'abc.txt') => last mod date 252 | - (BOOL)syncStepWithRemoteFiles:(NSDictionary*)remoteFiles andRevs:(NSDictionary*)remoteRevs { 253 | NSDictionary* localFiles = [self getLocalStatus]; // Get the local filesystem 254 | [[NSUserDefaults standardUserDefaults] synchronize]; // Make sure the user defaults data is up to date 255 | 256 | NSMutableSet* all = [NSMutableSet set]; // Get a complete list of all files both local and remote 257 | [all addObjectsFromArray:localFiles.allKeys]; 258 | [all addObjectsFromArray:remoteFiles.allKeys]; 259 | for (NSString* file in all) { 260 | NSDate* local = [localFiles objectForKey:file]; 261 | NSDate* remote = [remoteFiles objectForKey:file]; 262 | BOOL lastSyncExists = [self lastSyncExists:file]; 263 | if (local && remote) { 264 | // File is in both places, but are the dates the same? 265 | double delta = local.timeIntervalSinceReferenceDate - remote.timeIntervalSinceReferenceDate; 266 | BOOL same = ABS(delta)<2; // If they're within 2 seconds, that's close enough to be the same 267 | if (!same) { 268 | // Dates are different, so we need to do something 269 | // If this was the proper algorithm, we'd check to see if both had changed since the last sync 270 | // And if so, keep both and rename the older one '*_conflicted' 271 | if (local.timeIntervalSinceReferenceDate > remote.timeIntervalSinceReferenceDate) { 272 | // Local is newer 273 | // So send the local file to dropbox, overwriting the existing one with the given 'rev' 274 | [self startTaskUpload:file rev:[remoteRevs objectForKey:file]]; 275 | return NO; 276 | } else { 277 | // Remote is newer 278 | // So download the file 279 | [self startTaskDownload:file]; 280 | return NO; 281 | } 282 | } 283 | } else { // Not in both places 284 | // Say at the end of last sync, it would be in all 3 places: local, remote, and sync 285 | if (remote && !local) { 286 | // Dropbox has it, we don't 287 | // If it was added to db since last sync, it won't be in our sync list, so add it local 288 | // If it was removed locally since last sync, it'll be in our sync list, so remove from db 289 | // If never been synced, it won't be in our sync list, so add it locally 290 | if (lastSyncExists) { 291 | // Remove from dropbox 292 | [self lastSyncRemove:file]; // Clear the 'last sync' for just this file, so we don't try deleting it again 293 | [self startTaskRemoteDelete:file]; 294 | return NO; 295 | } else { 296 | // Download it 297 | [self startTaskDownload:file]; 298 | return NO; 299 | } 300 | } 301 | if (local && !remote) { 302 | // We have it, dropbox doesn't 303 | // If it was added locally since last sync, it won't be in our sync list, so upload it 304 | // If it was deleted from db since last sync, it will be in our sync list, so delete it locally 305 | // If never synced, it won't be in our sync list, so upload it 306 | if (lastSyncExists) { 307 | [self lastSyncRemove:file]; // Clear the 'last sync' for just this file, so we don't try deleting it again 308 | [self startTaskLocalDelete:file]; // Delete locally 309 | return NO; 310 | } else { 311 | // Upload it. 'rev' should be nil here anyway. 312 | [self startTaskUpload:file rev:[remoteRevs objectForKey:file]]; 313 | return NO; 314 | } 315 | } 316 | } 317 | } 318 | return YES; // Nothing needs doing 319 | } 320 | 321 | #pragma mark - Callbacks for the load-remote-folder-contents 322 | 323 | // Remove the leading slash 324 | - (NSString*)noSlash:(NSString*)file { 325 | return [file stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"/"]]; 326 | } 327 | 328 | // Called by dropbox when the metadata for a folder has returned 329 | - (void)restClient:(DBRestClient*)_client loadedMetadata:(DBMetadata*)metadata { 330 | NSMutableDictionary* remoteFiles = [NSMutableDictionary dictionary]; 331 | NSMutableDictionary* remoteFileRevs = [NSMutableDictionary dictionary]; 332 | 333 | for (DBMetadata* item in metadata.contents) { 334 | if (item.isDirectory) { 335 | // Ignore directories for simplicity's sake 336 | } else { 337 | [remoteFiles setObject:item.lastModifiedDate forKey:[self noSlash:item.path]]; 338 | [remoteFileRevs setObject:item.rev forKey:[self noSlash:item.path]]; 339 | } 340 | } 341 | 342 | // Now do the comparisons to figure out what needs doing 343 | BOOL allComplete = [self syncStepWithRemoteFiles:remoteFiles andRevs:remoteFileRevs]; 344 | 345 | if (allComplete) { // All done - nothing to do! 346 | [self internalShutdownSuccess]; 347 | } 348 | } 349 | - (void)restClient:(DBRestClient*)client metadataUnchangedAtPath:(NSString*)path { 350 | [self internalShutdownFailed]; 351 | } 352 | - (void)restClient:(DBRestClient*)client loadMetadataFailedWithError:(NSError*)error { 353 | [self internalShutdownFailed]; 354 | } 355 | 356 | #pragma mark - Singleton management 357 | 358 | + (CHBgDropboxSync*)i { 359 | if (!bgDropboxSyncInstance) { 360 | bgDropboxSyncInstance = [[CHBgDropboxSync alloc] init]; 361 | } 362 | return bgDropboxSyncInstance; 363 | } 364 | 365 | #pragma mark - Publicly accessible stuff you should access from your app delegate 366 | 367 | // Call me in your app delegate's applicationDidBecomeActive (eg at startup and become-active) and when you link 368 | // and basically any time you've changed data and want to sync again 369 | + (void)start { 370 | if (![[DBSession sharedSession] isLinked]) return; // Not linked, so nothing to do 371 | [[self i] startup]; 372 | } 373 | 374 | // Call me from your app delegate when your app closes/goes to the background/unpairs 375 | + (void)forceStopIfRunning { 376 | [bgDropboxSyncInstance internalShutdownForced]; 377 | } 378 | 379 | // Called when they pair or unpair or restore a backup, clears the lastsync status so we dont inadvertantly delete things next time we sync 380 | + (void)clearLastSyncData { 381 | [[self i] lastSyncClear]; // Clear the last sync status so 382 | // Since last sync status is only used to justify deletes, it is safe to clear (it'll only possibly cause data un-deletion) 383 | } 384 | 385 | @end 386 | -------------------------------------------------------------------------------- /DropboxClearer.h: -------------------------------------------------------------------------------- 1 | // 2 | // DropboxClearer.h 3 | // Passwords 4 | // 5 | // Created by Chris on 10/03/12. 6 | // 7 | 8 | #import 9 | #import "DropboxSDK.h" 10 | 11 | typedef void(^dropboxCleared)(BOOL success); 12 | 13 | @interface DropboxClearer : NSObject 14 | 15 | + (void)doClear:(dropboxCleared)complete; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /DropboxClearer.m: -------------------------------------------------------------------------------- 1 | // 2 | // DropboxClearer.m 3 | // Passwords 4 | // 5 | // Created by Chris on 10/03/12. 6 | // 7 | 8 | #import "DropboxClearer.h" 9 | #import "DropboxSDK.h" 10 | 11 | @interface DropboxClearer() { 12 | DBRestClient* client; 13 | int successfulDeletesTogo; 14 | } 15 | @property(copy) dropboxCleared complete; 16 | @end 17 | 18 | @implementation DropboxClearer 19 | @synthesize complete; 20 | 21 | #pragma mark - Completion / cleanup 22 | 23 | - (void)finish:(BOOL)success { 24 | // Autorelease the client using the following two lines, because we don't want it to release *just yet* because it probably called the function that called this, and would crash when the stack pops back to it. 25 | // http://stackoverflow.com/questions/9643770/whats-the-equivalent-of-something-retain-autorelease-in-arc 26 | __autoreleasing DBRestClient* autoreleaseClient = client; 27 | [autoreleaseClient description]; 28 | 29 | // Free the client 30 | client.delegate = nil; 31 | client = nil; 32 | 33 | // Call the block 34 | if (complete) { 35 | complete(success); 36 | complete = nil; 37 | } 38 | [[self class] cancelPreviousPerformRequestsWithTarget:self]; // Cancel the 'timeout' message that was intended for memory management. This'll free self. 39 | } 40 | 41 | - (void)timeout { 42 | [self finish:NO]; 43 | } 44 | 45 | #pragma mark - Callbacks upon deletion 46 | 47 | - (void)restClient:(DBRestClient *)client deletedPath:(NSString *)path { 48 | successfulDeletesTogo--; 49 | if (successfulDeletesTogo<=0) { 50 | [self finish:YES]; 51 | } 52 | } 53 | - (void)restClient:(DBRestClient *)client deletePathFailedWithError:(NSError *)error { 54 | [self finish:NO]; 55 | } 56 | 57 | #pragma mark - Getting the list of files to erase 58 | 59 | // Called by dropbox when the metadata for a folder has returned 60 | - (void)restClient:(DBRestClient*)_client loadedMetadata:(DBMetadata*)metadata { 61 | successfulDeletesTogo=0; 62 | for (DBMetadata* item in metadata.contents) { 63 | successfulDeletesTogo++; 64 | [client deletePath:item.path]; 65 | } 66 | if (!metadata.contents.count) { // All done - nothing to do! 67 | [self finish:YES]; 68 | } 69 | } 70 | - (void)restClient:(DBRestClient*)client metadataUnchangedAtPath:(NSString*)path { 71 | [self finish:NO]; 72 | } 73 | - (void)restClient:(DBRestClient*)client loadMetadataFailedWithError:(NSError*)error { 74 | [self finish:NO]; 75 | } 76 | 77 | #pragma mark - Starting the process 78 | 79 | - (void)start { 80 | client = [[DBRestClient alloc] initWithSession:[DBSession sharedSession]]; 81 | client.delegate = self; 82 | [client loadMetadata:@"/"]; 83 | } 84 | 85 | // Create a dropbox clearer that calls your completion block when it's done. Handles memory for you. 86 | + (void)doClear:(dropboxCleared)complete { 87 | if ([[DBSession sharedSession] isLinked]) { 88 | DropboxClearer* dc = [[DropboxClearer alloc] init]; 89 | dc.complete = complete; 90 | [dc performSelector:@selector(timeout) withObject:nil afterDelay:30]; // A timeout, also this makes the runloop retain the clearer for us 91 | [dc start]; 92 | } else { 93 | complete(YES); // Not linked 94 | } 95 | } 96 | 97 | @end 98 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | CHBgDropboxSync 2 | =============== 3 | 4 | Obj-C / iPhone / iOS library for background syncing the contents of a folder to Dropbox, just like the 'magical' way that the desktop version automatically keeps everything in sync, by Chris Hulbert - chris.hulbert@gmail.com 5 | 6 | * https://github.com/chrishulbert/CHBgDropboxSync 7 | * Extracted from my skeleton key app: http://www.skeletonkeyapp.com/ 8 | * You're 100% free to use this: MIT license. Thoroughly tested by me, but no guarantees are made - so test it thoroughly for yourself before blindly trusting it! 9 | * This code all uses ARC for memory management. 10 | * This was written to go with the Dropbox SDK 1.1, however i'd recommend using whatever is latest here: https://www.dropbox.com/developers/reference/sdk 11 | * You'll need the ConciseKit headers: https://github.com/petejkim/ConciseKit 12 | * I'm happy to help if you email with any questions! 13 | * You really should be familiar with the Dropbox SDK before using this... 14 | 15 | CHBgDropboxSync 16 | --------------- 17 | 18 | Performs a 2-way sync process designed to either download or upload changes as necessary. Uses the NSUserDefaults to keep track of local files to help it decide whether to delete or transfer if a file is present one one side but not on the other. 19 | 20 | To use this, add the Dropbox API to your app as you normally would, creating an interface to link (and unlink) as normal. Use this class to sync a local folder (typically your Documents folder in an iOS app) to your app's folder on Dropbox in the background without user intervention. 21 | 22 | When dropbox linking is complete, you should call the 'clearLastSyncData' function before starting a sync, like so in your app delegate: 23 | 24 | - (BOOL)application:(UIApplication *)application 25 | handleOpenURL:(NSURL *)url { 26 | if ([[DBSession sharedSession] handleOpenURL:url]) { 27 | if ([[DBSession sharedSession] isLinked]) { 28 | NSLog(@"App linked successfully!"); 29 | // At this point you can start making Dropbox API calls 30 | 31 | [CHBgDropboxSync clearLastSyncData]; 32 | [CHBgDropboxSync start]; 33 | } 34 | return YES; 35 | } 36 | // Add whatever other url handling code your app requires here 37 | return NO; 38 | } 39 | 40 | Whenever your app becomes active, you should perform another sync, in case there is new data on Dropbox. Again, in your app delegate: 41 | 42 | // Gets called on app startup and returning to the app 43 | - (void)applicationDidBecomeActive:(UIApplication *)application { 44 | [CHBgDropboxSync start]; // Start the sync 45 | } 46 | 47 | If, in your particular app, you need to forcibly clear the remote dropbox data (i had to in the skeleton key app if you changed the master password therefore need to re-encrypt *every* file), you can use code like below. It stops any running sync, clears the remote dropbox and the sync state, performs local changes, and then re-syncs: 48 | 49 | [CHBgDropboxSync forceStopIfRunning]; // Stop syncing 50 | 51 | // Show a 'clearing' display 52 | UIAlertView* clearing = [[UIAlertView alloc] initWithTitle:nil 53 | message:@"Clearing Dropbox" 54 | delegate:nil 55 | cancelButtonTitle:nil 56 | otherButtonTitles:nil]; 57 | [clearing show]; 58 | 59 | [CHBgDropboxSync clearLastSyncData]; // Clear last sync data 60 | // *first* so if anything borks, nothing will be deleted next sync 61 | 62 | // Erase everything on dropbox 63 | [DropboxClearer doClear:^(BOOL success) { 64 | 65 | // Hide the 'clearing' alert 66 | [clearing dismissWithClickedButtonIndex:0 animated:YES]; 67 | 68 | if (success) { 69 | // ..Perform your operations on local data.. 70 | [CHBgDropboxSync start]; // Start sync again 71 | } else { 72 | [[[UIAlertView alloc] initWithTitle:@"Error" 73 | message:@"Couldn't clear dropbox" 74 | delegate:nil 75 | cancelButtonTitle:@"Ok" 76 | otherButtonTitles:nil] show]; 77 | } 78 | 79 | }]; 80 | 81 | Any time you've saved a local file and it would make sense to re-sync immediately, do this: 82 | 83 | [CHBgDropboxSync start]; 84 | 85 | If your user unlinks the app from their dropbox account, stop the sync and clear its sync state: 86 | 87 | [CHBgDropboxSync forceStopIfRunning]; 88 | [CHBgDropboxSync clearLastSyncData]; 89 | [[DBSession sharedSession] unlinkAll]; 90 | 91 | If you want to make your app listen for any local changes that the sync makes, such as downloading a new/updated file or deleting a file, listen for the "CHBgDropboxSyncUpdated" notification. 92 | 93 | DropboxClearer 94 | -------------- 95 | 96 | This helper class isn't really needed, only to be used if your app (like mine) needs the ability to clear out the remote dropbox. If you don't need that feature you can leave this out. --------------------------------------------------------------------------------