Files disappearing from NSLibraryDirectory - ios

I'm storing some files in the Library directory in an iOS app, using the following methods to construct it. In the end, I can call [MyClass dataDirectory] to do my file handling and all is well. I've recently discovered, however, that some files seem to be mysteriously disappearing out of this directory. According to the documentation, this should not be the case. Is this a safe place to store persistent files?
The console output of this directory is: ~/var/mobile/Containers/Data/Application/{id}/Library/Data
+ (NSString*)libraryDirectory
{
return [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) lastObject];
}
+ (NSString*)dataDirectory
{
NSString* dir = [[self libraryDirectory] stringByAppendingPathComponent:#"Data"];
BOOL isDir=NO;
NSError * error = nil;
NSFileManager *fileManager = [NSFileManager new];
if (![fileManager fileExistsAtPath:dir isDirectory:&isDir] && isDir)
{
[[NSFileManager defaultManager] createDirectoryAtPath:dir
withIntermediateDirectories:YES
attributes:nil
error:&error];
}
[self addSkipBackupAttributeToItemAtURL:[NSURL fileURLWithPath:dir isDirectory:YES]];
if (error != nil) {
DDLogError(#"Fatal error creating ~/Library/Data directory: %#", error);
}
return dir;
}
And the skip method:
+ (BOOL)addSkipBackupAttributeToItemAtURL:(NSURL *)URL
{
if ([[NSFileManager defaultManager] fileExistsAtPath:[URL path]])
{
assert([[NSFileManager defaultManager] fileExistsAtPath: [URL path]]);
NSError *error = nil;
BOOL success = [URL setResourceValue: [NSNumber numberWithBool: YES]
forKey: NSURLIsExcludedFromBackupKey error: &error];
if(!success){
DDLogError(#"Error excluding %# from backup %#", [URL lastPathComponent], error);
}
return success;
}
return YES;
}

In the code you posted, the first problem is here:
if (![fileManager fileExistsAtPath:dir isDirectory:&isDir] && isDir)
At the point where this is evaluated, isDir will default to NO, and will be set to NO if the file does not exist or is not a directory. This will prevent the directory from being created. Remove && isDir or change to || !isDir to get the logic you want.
Now on to your original question:
Is this (a subdirectory of NSLibraryDirectory) a safe place to store persistent files?
Yes. NSLibraryDirectory is backed up by default. To comply with the iOS Data Storage Guidelines an application should not store user-created data in that location, but it is a safe place to store application data. NSApplicationSupportDirectory is a directory that is generally within the NSLibraryDirectory, and is the preferred place to store this kind of data. Data within that location will be backed up, and will be migrated during application and OS updates.
The iOS Data Storage Guidelines, File System Programming Guide, and App Programming Guide for iOS all provide guidance on where to put files, and how they will be backed up from standard file system locations.
Unless those files have had their NSURLIsExcludedFromBackupKey/kCFURLIsExcludedFromBackupKey resource metadata value altered. Then it gets much more complicated.
Files 'Excluded From Backup'
Generally, if a file outside of a Documents directory can be backed up, the system assumes it can also purge it under low space or other conditions. This is why setting NSURLIsExcludedFromBackupKey to YES on a file allows the file to persist even in low storage conditions. If your application sets NSURLIsExcludedFromBackupKey to YES for a file, your application assumes responsibility for the life of that file.
The catch here is that the backup process and the purge process do not follow the same logic. Apple's documentation indicates that for the purposes of controlling the backup behavior, it is possible to set NSURLIsExcludedFromBackupKey on a directory. The children of that directory will effectively inherit that resource value (in practice, this may not be accurate). The purge process, however, does not seem to have the same behavior. It may not check the backup exclusions of the parent directories and apply it to children, and as a result if a file does not have NSURLIsExcludedFromBackupKey explictly set it may be purged.
This gets even more complicated. If you were to read the documentation for the constant NSURLIsExcludedFromBackupKey you would see:
Some operations commonly made to user documents cause this property to be reset to false; consequently, do not use this property on user documents.
This actually applies to much more than user documents. For example, if you were to perform an atomic write on a file such as:
[thing writeToURL:URL atomically:YES encoding:NSUTF8StringEncoding error:&error]
If the file at URL had NSURLIsExcludedFromBackupKey set to YES before the write, it would now appear to be set to NO. An atomic write like this will first create a temporary file, write to that, and replace the original with the new file. In doing so, file and URL resource flags are not preserved. The original file had the NSURLIsExcludedFromBackupKey resource value set, the newly created file at the same location now does not. This is just one example; many Foundation APIs perform atomic writes like this implictly.
There are scenarios where this gets even more complex. When an application is updated it is installed into a new location with a new application container path. Data inside the old application container is migrated. There are few guarantees regarding what may or may not be migrated as part of the update process. It may be everything, it may be only some things. In particular there are is no guidance concerning how files or directories marked with the NSURLIsExcludedFromBackupKey resource attribute will be treated. In practice it seems that these are often the least likely files to be migrated, and when they are migrated the NSURLIsExcludedFromBackupKey attribute is rarely preserved.
OS updates are also an issue. Historically Over-The-Air updates have been problematic and have caused the NSURLIsExcludedFromBackupKey resource attribute to be effectively cleared or ignored. A "major" OS update will clear the device and restore from a backup - which is equivalent to migrating to new hardware. Files marked with the NSURLIsExcludedFromBackupKey resource attribute will not be migrated, and the application will have to re-create them.
Update scenarios are described in TechNote 2285: Testing iOS App Updates
Because of this, when using NSURLIsExcludedFromBackupKey it is generally best to set the value on every access, and as always should be done through the File Coordination APIs (unless you are writing to a shared group container, which is an entirely different set of issues). If the NSURLIsExcludedFromBackupKey resource attribute value is lost files can be purged at any time. Ideally an application should not depend on the NSURLIsExcludedFromBackupKey or how the OS may (or may not!) handle it, but instead be designed such that the data could be recreated on demand. That may not always be possible.
It's clear from your question and the code that you posted that you are somewhat dependant on NSURLIsExcludedFromBackupKey ensuring that your file(s) have an application-controlled lifetime. As you can see from the above, that may not always be the case: there are many, many common scenarios where that resource attribute value can disappear, and with it your files.
It is also worth noting that NSFileProtection attributes work the same way, and can disappear in the same scenarios (and a few more).
TL;DR; What should I do?
Based on your question, code, and the description of the behavior you are seeing:
Setting the NSURLIsExcludedFromBackupKey value on the directory containing the file(s) you are interested in preserving may not be enough to prevent them from being purged. It would be wise to set NSURLIsExcludedFromBackupKey on every access to the actual files, rather than just a parent directory. Also attempt to ensure this resource value is set after any write to the file, especially through a high level API that may be doing atomic writes, etc.
All NSFileManager and file reading/writing operations should use file coordination. Even in an application that is single threaded there will be other processes interacting with "your" files. Processes like the daemons that run backups or purge files during low space conditions. Between your -fileExistsAtPath: and the -setResourceValue:forKey:error: another process could alter, delete, or move your file and its attributes. -setResourceValue:forKey:error: will actually return YES and no error in many cases where it did nothing, like the file not existing.
Files and directories marked with NSURLIsExcludedFromBackupKey are the responsibility of the application to manage. The application should still purge those files or their contents at some appropriate time, or set limits on their growth. If you look at the per-application disk usage information on a device, you can probably guess the names of some applications that do not do this correctly.
Test update scenarios as described in TechNote 2285: Testing iOS App Updates. often. Ideally the iOS Simulator would have a "Simulate Low Disk Space" capability similar to simulating memory warnings, but at this time it does not.
If at all possible, alter application logic to recreate these files if they go missing.

In the documentation you linked it is stated that
Critical data should be stored in the /Documents directory. Critical data is any data that cannot be recreated by your app, such as user documents and other user-generated content.
It is also mentioned that
Cached data should be stored in the /Library/Caches directory. Examples of files you should put in the Caches directory include (but are not limited to) database cache files and downloadable content, such as that used by magazine, newspaper, and map apps. Your app should be able to gracefully handle situations where cached data is deleted by the system to free up disk space.
The directory you are using is not explicitly mentioned for storing user data, it is used by the system and is not save for your data. It's guarantied to be untouched by an update of your app, but that's it
To find the documents folder you could do something like
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsFolderPath = [paths firstObject];

Related

download file to update-independent directory in objective c

I'm developing an IOS app which download some video and save it, but after update app or reinstall it the directory of app change, and app can not access to previous downloaded files.
Is there any way to save downloaded file to location out of application domain directory?
the code for generating path to save file is.
NSURL *documentsDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];
return [documentsDirectoryURL URLByAppendingPathComponent:[response suggestedFilename]];
I cannot verify the update part of your question.
I use the application's Documents all the time and after an update that is untouched, so I can still use any files added to it in a previous version.
For the reinstalling (i.e. first delete the app, then re-download it from the AppStore and run it) there is obviously no such way, unless you consider what ravi.p suggested in his comment (adding it to PhotosAlbum). That would beat the entire purpose of a reinstall. If a user deletes the app they want to delete all its data. That's why the warning dialog specifically makes that clear. If you could circumvent this would beat the intention, wouldn't it?
The previous Documents directory isn't changed in this case, it is deleted and on the reinstall a new one is created.
I didn't check it recently, but I believe even the full path of the Documents directory doesn't change after an update, including the hashed part that is created by the sandboxing mechanism. My guess is that you're either confusing something with the update process or simply get a different file name in the newer app version.
Edit:
Okay, so apparently the application's folders can change on an update, at least according to what you further explained in your comment. :)
Then I am wondering how you can find your sqlite file again, but in ay way there is an easy solution to your problem:
Do not save the full path in your database. That is bad practice anyways, since the method you already used to get it in the first place (after downloading the file) is meant to be used for accessing the folder. So you only save
[response suggestedFilename]
in your database (going from your posted code).
In places where you need to access the file you then simply rebuild your path in just the same way you do in the first line of code you posted.
You can even write a convenience method for this in some place (a model class or the class taking care about DB access).
And by the way: Again, updating and reinstalling are two different things, especially the process in which Xcode copies your development build onto your development device is a different one from how users ultimately update your app from the store.

How long do files stay in the documents directory (iOS)

If I save a file to the documents directory in a folder, how long does it stay on the disk? Forever (in that case I would have to delete it manually if I wish to do so) or it is deleted every time the app closes?
Files in the documents directory will persist until either the user deletes the app, resets their device or you remove the files in code. Also note however that the contents of the documents directory are backed up by iTunes so could persist longer than you would otherwise expect.
On the other hand, the tmp directory is not so persistent and its contents won't necessarily survive an app relaunch (they're also not backed up by iTunes) so you could use this if you don't want persistant storage. /Library/Caches/ is similar in that it isn't backed up but it is persistent. From the docs regarding tmp:
The system will periodically purge these files [in the tmp folder] when your app is not running; therefore, you cannot rely on these files persisting after your app terminates.
Basically, if you want to store something for a short period and it doesn't matter if it's deleted by the OS, use tmp. If you want something that will persist app launches and whatnot but still won't be backed up via iTunes, use the cache folder in Library. It you want something that persists and is backed up, use documents.
Extra info based on comment
If the images should disappear when the app is relaunched then you should use tmp. But take note that the tmp directory is not guaranteed to be cleared on an app relaunch. It's certainly possible that the images will still be there the next time you load the app. It's a bit of a gamble really. If it is vital to kill the images then wipe them manually somewhere suitable:
NSArray* tmpDirectory = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:NSTemporaryDirectory() error:NULL];
for (NSString *file in tmpDirectory) {
[[NSFileManager defaultManager] removeItemAtPath:[NSString stringWithFormat:#"%#%#", NSTemporaryDirectory(), file] error:NULL];
}

iCloud file versions and mystery files

I have been trying to retrofit a large app to have it store its files in iCloud. The app is not really "Document-based" but the files may be thought of as configuration or preference files.
To be honest I haven't followed all the Apple iCloud guidelines. Some really don't fit in with the flow of the app. When the app starts I read the files from the ubiquity directory (using normal file reads) and when I write them I use a normal file write to the ubiquity directory. When the app start, I also call:
[fileManager startDownloadingUbiquitousItemAtURL:url error:&error];
I do have an NSFilePresenter watching for changes in the ubiquity directory. It notifies me of file changes but there are never any conflict notifications.
My problem is that often when I upload a file to iCloud, it will create a separate file with a number appended. E.g.
MyFile.skyset
MyFile 2 .skyset
These seem to show up when more than one app has been writing MyFile.skyset to the ubiquity directory.
They don't seem to be conflicted file versions. If I use NSFileVersion to look for conflicts and other versions, I only see the one version of MyFile.skyset and it is not in conflict.
I can't find any documentation that explains what this "versioned" file is and what to do about it. Any thoughts as to what is going on here?
Well, I don't fully understand the "versions" but the problem was solved by changing how I updated the files in the ubiquity directory.
I changed from:
[fileMgr removeItemAtPath:dstPath error:nil];
[fileMgr copyItemAtPath:srcPath toPath:dstPath error:&error];
To:
NSData *data = [NSData dataWithContentsOfFile:srcPath];
[data writeToFile:dstPath atomically:NO];
I looks like the operation of deleting the file before copying the updated information to the ubiquity directory was confusing the system. After switching to the second form I'm no longer seeing this problem.
Hope this answer helps someone else.

Replace Data Container of an iOS .ipa File

I have an app that downloads a whole bunch of data from over 100+ APIs upon successful login. I successfully download the data, and then use iExplorer to extract the data container folders (Documents, Library and Tmp) from the fully loaded application.
I would like to take a blank version of the original app, in .ipa format, and insert those data container folders into that fully loaded .ipa file. Then I will be able to take this new fully loaded .ipa, and use a deployment software to deploy it to a bunch of local user's devices. So everyone will have this fully loaded app.
Please, has anyone done this? Please provide some feedback, and don't argue with my methodology, because this has be done this way due to requirements. Maybe there is a step I'm missing? I'm not sure.
With the source code in hand, you can run the app in the simulator (no need for iExplorer), wait for it to download all the files and browse to the folder on your computer where the app was installed.
From there you can put aside any files you want along with their respective folders. If you're using Coredata there should be a SQLITE database file there somewhere (typically in your Application Support folder) and this might be all you need but it is hard to tell without looking at your implementation details.
Once you have the files you need set aside, add them to the app bundle via Xcode and create code to check whether files already exist (in which case you don't want to replace them), and if not copy all files needed from the bundle into their respective folders.
Here's some semi pseudo-code for you:
NSDictionary *userPrefs = [[NSUserDefaults standardUserDefaults] objectForKey:self.email];
if (![userPrefs[kInitialSetupCompleted] boolValue])
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *destinationFilePath = ...
NSURL *seedFilePath = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:kCoreDataStoreName ofType:nil]];
NSError* err = nil;
if (![fileManager copyItemAtURL:seedPath toURL:destinationFilePath error:&err]) {
NSLog(#"Could not copy seed data. error: %#", err);
// Deal with error
} else {
// Set user defaults kInitialSetupCompleted to YES
}
}

How can I prevent a Core Data database with external files from being backed up?

We have an app that uses Core Data to store data. Some of the data comes from a server, and is variable sized, and while it's generally small, can also be quite large (a few megabytes). We're using a Binary Data field with "Allows External Storage" enabled to store this data.
Our app was rejected for a Rule 2.23 violation, because we're storing data that we could re-download from the server and not marking it as to be excluded from backups.
Okay, fair enough, so I want to mark our database to be excluded from backup. Marking the database itself is easy enough:
BOOL succ = [storeURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:&error];
if (!succ) {
NSLog(#"Error setting %# to be excluded from backups: %#", storeURL, error);
}
But this only covers the SQLite file itself. When Core Data stores external files, it stores them in a separate directory:
.../Private Documents/MyDatabase.sqlite
.../Private Documents/.MyDatabase_SUPPORT/_EXTERNAL_DATA/(files here)
I need to flag the externally-stored files as not-for-backup. I could set NSURLIsExcludedFromBackupKey on the .MyDatabase_SUPPORT folder, but I don't see any supported way to get the name of this folder.
I could just build it myself, but I don't like depending on this implementation detail. Is there any supported way to find out where Core Data is storing the external files for a database?
Create a subdirectory, create the database within that subdirectory, and then set the NSURLIsExcludedFromBackupKey attribute on the subdirectory. That way anything within the subdirectory, including the hidden SUPPORT directory, will be excluded from backup.

Resources