iOS Downloading multiple files in the background - ios

I have read few articles but could not find what I was looking for so here's my query.
I am downloading few files from the server and there is a case where the user locks his screen, in this case the ios device loses its network connectivity and the file sync fails.
I have read few article on NSURLSession but it is available from iOS 7 onwards and the app I am working on supports from iOS 6 and later.
So is there a way where I can download 20 or 30 files in the background or when the user hits the lock screen in a generic fashion without having to worry about which OS version I am supporting.
As of now I have read that we have 30 seconds to perform network activity so is there a limitation to the number of server calls in these 30 seconds?
About my code, I am having a class named as DownloadFiles which calls a service and the service returns me an array of fileURL and using NSData I am fetching these files and saving them in the doc directory, so while implementing the background call thing do I need to pass the index of my array which will detect the current file which is downloading and then carry on from the next index.
for(NSDictionary *dict in filearray) {
NSString *fileURL = [[dict valueForKey:#"FileURL"]stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSData *fileData = [NSData dataWithContentsOfURL:theFileURL];
if (fileData.length==0 || fileData==nil || theFileURL==nil) {
NSLog(#"empty file URL = %#",theFileURL);
}
if (fileData.length!=0){
BOOL savefile = [fileData writeToFile:[HTML_SERVER_FILES stringByAppendingPathComponent:[dict valueForKey:#"FileName"]] atomically:YES];
if (savefile!=YES) {
NSLog(#"Not saved file = %#",theFileURL);
}else{
NSLog(#"file saved at path %#",HTML_SERVER_FILES);
}
fileData = nil;
}
}
Please let me know what needs to be done in this case.

Related

iOS Background Transfer - com.apple.nsurlsessiond folder full of tmp files

We've written a media application that allows you to get a list of latest videos as json list using BACKGROUND FETCH
then it uses BACKGROUND TRANSFER to tell iOS to download the video one by one and go back to sleep and to wake the app when its done.
It does all that but we've noticed that Space Usage is growing and growing.
We added code to clear all downloaded videos but space usage stayed hi in settings.
We downloaded the app folders using Xcode > Organizer> Devices and found the BACKGROUND TRANSFER tmp folder was dull of tmp files.
Shouldn't these be getting cleared out
This is in general the code I use.
I think the main is I attach multiple DownloadTask(can be up to 30) to one background session. files vary in size from movies to pdfs.
NSURLSession * backgroundSession_ = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:identifier];
backgroundSession_ = [NSURLSession sessionWithConfiguration:urlSessionConfigurationBACKGROUND_
delegate:self
delegateQueue:[NSOperationQueue mainQueue]];
NSOperationQueue *mainQueue_ = [NSOperationQueue mainQueue];
NSURLSessionDownloadTask * downloadTask_ = [backgroundSession_ downloadTaskWithURL:url_];
downloadStarted_ = TRUE;
[downloadTask_ resume];
Try something like this before returning from didFinishDownloadingToURL:
// App's Documents directory path
NSString *docsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)firstObject];
// Creating the path for the downloaded file
docsPath = [docsPath stringByAppendingPathComponent:downloadTask.response.suggestedFilename];
// Moving the file from temp location to App's Documents directory
[[NSFileManager defaultManager] moveItemAtPath:location.path toPath:docsPath error:NULL];
The documentation states that you should "move the file to a permanent location in your app’s sandbox container directory before returning from this delegate method" (perhaps the Documents directory).
The temp files that gets cleared out after you return from didFinishDownloadingToURL (or if the download failed) - at the OS's discretion (usually under memory pressure).
I have the same issue but in a bit different circumstances:
on older devices (iPhone 4S or older) the app is usually killed during the background fetching by the OS. Probably to free memory. In this case the tmp files are retained (and untracked). Next time the app has an opportunity to fetch, new files are created... and this cycle goes on and on till the user recognises the app uses 4gb of storage space - and deletes it.
I haven't found the perfect solution yet - even after I set the background configuration's -NSURLSessionConfiguration URLCache to a custom one (documentation says it's nil by default) with a path the same directory (defaultCacheDir/com.apple.nsurlsessiond/...) was used - but made a cleanup method and use it when I sure there is no download in progress.
+ (BOOL)clearCache:(NSError * __autoreleasing *)error
{
__block BOOL successOnLegacyPath = NO;
__block NSError *errorOnLegacyPath = nil;
NSString *cacheDirPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
NSArray *allSubPaths = [[NSFileManager defaultManager] subpathsAtPath:cacheDirPath];
if (!allSubPaths) {
NSLog(#"No subpaths of cache:\n%#", cacheDirPath);
} else {
[allSubPaths enumerateObjectsUsingBlock:^(NSString *subpath, NSUInteger idx, BOOL *stop) {
static NSString * const kNSURLSessionPathComponent = #"nsurlsession"; // this is a non-documented way, Uncle Apple can change the path at any time
if ([subpath containsString:kNSURLSessionPathComponent]) {
successOnLegacyPath = [[NSFileManager defaultManager] removeItemAtPath:[cacheDirPath stringByAppendingPathComponent:subpath]
error:&errorOnLegacyPath];
if (!successOnLegacyPath) {
NSLog(#"Error while deleting cache subpath:\n%#\nError:\n%#", subpath, errorOnLegacyPath);
}
// First we find is the root > bail out
*stop = YES;
}
}];
}
if (!successOnLegacyPath && !errorOnLegacyPath) {
// Couldn't find the nsurlsession's cache directory
if (error) *error = [NSError errorWithDomain:NSCocoaErrorDomain
code:NSFileNoSuchFileError
userInfo:nil];
// OR
successOnLegacyPath = YES;
}
return successOnLegacyPath;
}
This is not a solution and this is recommended to use if no download is in progress. Haven't tested what's happening if there are running downloads and trying to delete the tmp files.
Even if I find a solution, the previously created tmp files still remain untracked so those need to be deleted by a method like this.
Btw, this seems to be the same question - without conclusion.

Using GCD for notification of end of file write? [duplicate]

I'm working on iOS project that supports iTunes file sharing feature. The goal is realtime tracking incoming/changed data's.
I'm using (kinda modified) DirectoryWatcher class from Apple's sample code
and also tried this source code.
The data is NSBundle (*.bundle) and some bundles are in 100-500 MB ranges, depends on its content, some video/audio stuff. The bundles has xml based descriptor file in it.
The problem is any of these codes above fires notification or whatever else when the data just started copying and but not when the copy/change/remove process finished completely.
Tried next:
checking file attributes:
NSDictionary *fileAttrs = [[NSFileManager defaultManager] attributesOfItemAtPath:[contURL path] error:nil];
BOOL fileBusy = [[fileAttrs objectForKey:NSFileBusy] boolValue];
looking for the fileSize changes:
dispatch_async(_checkQueue, ^{
for (NSURL *contURL in tempBundleURLs) {
NSInteger lastSize = 0;
NSDictionary *fileAttrs = [[NSFileManager defaultManager] attributesOfItemAtPath:[contURL path] error:nil];
NSInteger fileSize = [[fileAttrs objectForKey:NSFileSize] intValue];
do {
lastSize = fileSize;
[NSThread sleepForTimeInterval:1];
fileAttrs = [[NSFileManager defaultManager] attributesOfItemAtPath:[contURL path] error:nil];
fileSize = [[fileAttrs objectForKey:NSFileSize] intValue];
NSLog(#"doing job");
} while (lastSize != fileSize);
NSLog(#"next job");
}
);
any other solutions?
The solution above works great for bin files, but not for .bundle (as .bundle files are directory actually). In order to make it work with .bundle, you should iterate each file inside .bundle
You can use GCD's dispatch sources mechanism - using it you can observe particular system events (in your case, this is vnode type events, since you're working with file system).
To setup observer for particular directory, i used code like this:
- (dispatch_source_t) fileSystemDispatchSourceAtPath:(NSString*) path
{
int fileDescr = open([path fileSystemRepresentation], O_EVTONLY);// observe file system events for particular path - you can pass here Documents directory path
//observer queue is my private dispatch_queue_t object
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, fileDescr, DISPATCH_VNODE_ATTRIB| DISPATCH_VNODE_WRITE|DISPATCH_VNODE_LINK|DISPATCH_VNODE_EXTEND, observerQueue);// create dispatch_source object to observe vnode events
dispatch_source_set_registration_handler(source, ^{
NSLog(#"registered for observation");
//event handler is called each time file system event of selected type (DISPATCH_VNODE_*) has occurred
dispatch_source_set_event_handler(source, ^{
dispatch_source_vnode_flags_t flags = dispatch_source_get_data(source);//obtain flags
NSLog(#"%lu",flags);
if(flags & DISPATCH_VNODE_WRITE)//flag is set to DISPATCH_VNODE_WRITE every time data is appended to file
{
NSLog(#"DISPATCH_VNODE_WRITE");
NSDictionary* dict = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil];
float size = [[dict valueForKey:NSFileSize] floatValue];
NSLog(#"%f",size);
}
if(flags & DISPATCH_VNODE_ATTRIB)//this flag is passed when file is completely written.
{
NSLog(#"DISPATCH_VNODE_ATTRIB");
dispatch_source_cancel(source);
}
if(flags & DISPATCH_VNODE_LINK)
{
NSLog(#"DISPATCH_VNODE_LINK");
}
if(flags & DISPATCH_VNODE_EXTEND)
{
NSLog(#"DISPATCH_VNODE_EXTEND");
}
NSLog(#"file = %#",path);
NSLog(#"\n\n");
});
dispatch_source_set_cancel_handler(source, ^{
close(fileDescr);
});
});
//we have to resume dispatch_objects
dispatch_resume(source);
return source;
}
I found two rather reliable (i.e. not 100% reliable but reliable enough for my needs) approaches, which only work in conjunction with polling the contents of the directory:
Check NSURLContentModificationDateKey. While the file is being transferred, this value is set to the current date. After transfer has finished, it is set to the value the original file had: BOOL busy = (-1.0 * [modDate timeintervalSinceNow]) < pollInterval;
Check NSURLThumbnailDictionaryKey. While the file is being transferred, this value is nil, afterwards it cointains a thumbnail, but probably only for file types from which the system can produce a thumbnail. Not a problem for me cause I only care about images and videos, but maybe for you. While this is more reliable than solution 1, it hammers the CPU quite a bit and may even cause your app to get killed if you have a lot of files in the import directory.
Dispatch sources and polling can be combined, i.e. when a dispatch source detects a change, start polling until no busy files are left.

How to know when iTunes has finished copying a file to the documents directory [duplicate]

I'm working on iOS project that supports iTunes file sharing feature. The goal is realtime tracking incoming/changed data's.
I'm using (kinda modified) DirectoryWatcher class from Apple's sample code
and also tried this source code.
The data is NSBundle (*.bundle) and some bundles are in 100-500 MB ranges, depends on its content, some video/audio stuff. The bundles has xml based descriptor file in it.
The problem is any of these codes above fires notification or whatever else when the data just started copying and but not when the copy/change/remove process finished completely.
Tried next:
checking file attributes:
NSDictionary *fileAttrs = [[NSFileManager defaultManager] attributesOfItemAtPath:[contURL path] error:nil];
BOOL fileBusy = [[fileAttrs objectForKey:NSFileBusy] boolValue];
looking for the fileSize changes:
dispatch_async(_checkQueue, ^{
for (NSURL *contURL in tempBundleURLs) {
NSInteger lastSize = 0;
NSDictionary *fileAttrs = [[NSFileManager defaultManager] attributesOfItemAtPath:[contURL path] error:nil];
NSInteger fileSize = [[fileAttrs objectForKey:NSFileSize] intValue];
do {
lastSize = fileSize;
[NSThread sleepForTimeInterval:1];
fileAttrs = [[NSFileManager defaultManager] attributesOfItemAtPath:[contURL path] error:nil];
fileSize = [[fileAttrs objectForKey:NSFileSize] intValue];
NSLog(#"doing job");
} while (lastSize != fileSize);
NSLog(#"next job");
}
);
any other solutions?
The solution above works great for bin files, but not for .bundle (as .bundle files are directory actually). In order to make it work with .bundle, you should iterate each file inside .bundle
You can use GCD's dispatch sources mechanism - using it you can observe particular system events (in your case, this is vnode type events, since you're working with file system).
To setup observer for particular directory, i used code like this:
- (dispatch_source_t) fileSystemDispatchSourceAtPath:(NSString*) path
{
int fileDescr = open([path fileSystemRepresentation], O_EVTONLY);// observe file system events for particular path - you can pass here Documents directory path
//observer queue is my private dispatch_queue_t object
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, fileDescr, DISPATCH_VNODE_ATTRIB| DISPATCH_VNODE_WRITE|DISPATCH_VNODE_LINK|DISPATCH_VNODE_EXTEND, observerQueue);// create dispatch_source object to observe vnode events
dispatch_source_set_registration_handler(source, ^{
NSLog(#"registered for observation");
//event handler is called each time file system event of selected type (DISPATCH_VNODE_*) has occurred
dispatch_source_set_event_handler(source, ^{
dispatch_source_vnode_flags_t flags = dispatch_source_get_data(source);//obtain flags
NSLog(#"%lu",flags);
if(flags & DISPATCH_VNODE_WRITE)//flag is set to DISPATCH_VNODE_WRITE every time data is appended to file
{
NSLog(#"DISPATCH_VNODE_WRITE");
NSDictionary* dict = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil];
float size = [[dict valueForKey:NSFileSize] floatValue];
NSLog(#"%f",size);
}
if(flags & DISPATCH_VNODE_ATTRIB)//this flag is passed when file is completely written.
{
NSLog(#"DISPATCH_VNODE_ATTRIB");
dispatch_source_cancel(source);
}
if(flags & DISPATCH_VNODE_LINK)
{
NSLog(#"DISPATCH_VNODE_LINK");
}
if(flags & DISPATCH_VNODE_EXTEND)
{
NSLog(#"DISPATCH_VNODE_EXTEND");
}
NSLog(#"file = %#",path);
NSLog(#"\n\n");
});
dispatch_source_set_cancel_handler(source, ^{
close(fileDescr);
});
});
//we have to resume dispatch_objects
dispatch_resume(source);
return source;
}
I found two rather reliable (i.e. not 100% reliable but reliable enough for my needs) approaches, which only work in conjunction with polling the contents of the directory:
Check NSURLContentModificationDateKey. While the file is being transferred, this value is set to the current date. After transfer has finished, it is set to the value the original file had: BOOL busy = (-1.0 * [modDate timeintervalSinceNow]) < pollInterval;
Check NSURLThumbnailDictionaryKey. While the file is being transferred, this value is nil, afterwards it cointains a thumbnail, but probably only for file types from which the system can produce a thumbnail. Not a problem for me cause I only care about images and videos, but maybe for you. While this is more reliable than solution 1, it hammers the CPU quite a bit and may even cause your app to get killed if you have a lot of files in the import directory.
Dispatch sources and polling can be combined, i.e. when a dispatch source detects a change, start polling until no busy files are left.

NSFilemanager uploads new file, but UIWebView shows cached version

An app I'm building needs to periodically check a known location for an updated file. It happens to be a PDF. If x amount of time has passed, the app needs to upload a new copy of the file. The following code snippet works. It downloads the file. But the app displays a cached version of the PDF instead of the new one. I confirmed this by looking in the app bundle. After this code runs, there is definitely a new file in the Documents directory. But in the bundle's tmp/DiskImageCache-[random gibberish string] there is a copy of the old version of the PDF - and that is what is being displayed by my UIWebView.
I searched the NSFileManager docs and of course, schmoogled up a storm, but I have not been able to find a way to get the app to show the new upload instead of the cached version of the PDF.
Thanks a ton for any assistance you can render with this problem.
-(void) checkFile:(NSString *)url andSaveTo:(NSString __autoreleasing *)filename {
NSFileManager *manager = [NSFileManager defaultManager];
NSError *error = nil;
NSDictionary *attributes = nil;
if ([manager fileExistsAtPath:filename]) {
attributes = [manager attributesOfItemAtPath:filename error:nil];
double updateInterval = [[attributes fileCreationDate] timeIntervalSinceNow];
cacheInterval = CacheInterval;
if (ABS(updateInterval) > CacheInterval) {
[self downloadFile:url andSaveTo:fileName];
}
}
}
you can always thwart caching by appending a random number to your url as a parameter
ex: address/yourfilehere.pdf?rand=0323094230948203984
and give your long random parameter a new randomly generated number each time

Check on app resume for server file changes?

I have a Data plist (conveniently named Data.plist) that is updated on launch of the app:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Determile cache file path
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *filePath = [NSString stringWithFormat:#"%#/%#", [paths objectAtIndex:0],#"Data.plist"];
NSString *dataURLString = #"http://link/to/Data.plist";
NSURL *dataURL = [[NSURL alloc] initWithString:dataURLString];
NSData *plistData = [NSData dataWithContentsOfURL:dataURL];
[plistData writeToFile:filePath atomically:YES];
NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:filePath];
NSLog(#"The bundle is %#", filePath);
self.data = dict;
// Configure and show the window
[window addSubview:[navigationController view]];
[window makeKeyAndVisible];
return YES;
}
I'd like to be able to have some way of checking the saved plist against the server plist - I've seen some implementations that use external libraries but there has to be something in the original iOS SDK. Any ideas? I've read whatever code I do end up using needs to be implemented in viewWillAppear but I'm not sure what that code is exactly.
Two things... first, dataWithContentsOfURL: and generally any of Apple's (temptingly convenient) <anything>WithContentsOfURL: methods are extremely unsafe in the real world. It's blocking which means that no other code will execute until your request succeeds or fails. That means that if the server isn't available or your device doesn't have internet or for some other reason cannot retrieve your data, your phone will sit there until either the iOS watchdog process kills your app for freezing for too long, or it just fails. Then the rest of your app that is expecting data will freak out because suddenly you have no data when your code assumes you should. This is one of many problems with synchronous requests.
I won't go into how to implement asynchronous requests, but head over to Apple's documentation or you can use a wrapper framework like http://allseeing-i.com/ASIHTTPRequest/ that does it for you. Also have a look at http://www.cocoabyss.com/foundation/nsurlconnection-synchronous-asynchronous/
To answer your actual question, you could have a tiny text file on your server with a version number or time stamp and download that along with your plist. on subsequent launches, you can pull down the time stamp/version number and compare it against the one you've got stored, and if the version on the server is more recent, then you pull it and save the new time stamp/version number.

Resources