iOS: flush all output files - ios

Is there an iOS method to flush all open output files-- so that when the call returns, all pending buffered data is written to persistent storage? I mean this in general terms-- where I don't have access to the specific file handles. I've tried sync() from Swift (a POSIX call) but that appears not guaranteed to actually make sure the data hits persistent storage before it returns (see http://pubs.opengroup.org/onlinepubs/009696899/functions/sync.html).

Not sure if this solves the problem in general, but it's addressing my need. Here's what I'm doing:
// Write to file (in my case, this is a log file where I don't have access to the file handle).
// I'm using logging to a file with SwiftyBeaver. https://github.com/SwiftyBeaver/SwiftyBeaver
DispatchQueue.main.async {
// Read from the file-- it has the flushed contents. I'm doing:
do {
let fileContents = try String(contentsOfFile: path)
} catch (let error) {
}
}
It looks like files are flushed at the end of the event loop.

Related

How to check the size of CoreData in Swift

I am storing data in CoreData. I want to know what is the size of whole CoreData in MBs. Basically the requirement is to clear the whole CoreData once it reaches 10 MB. I did not find any working answers for this. I have tried using below code but unable to get the size.
public func getSqliteStoreSize(forPersistentContainerUrl storeUrl: URL) -> String {
do {
let size = try Data(contentsOf: storeUrl)
if size.count < 1 {
print("Size could not be determined.")
return ""
}
let bcf = ByteCountFormatter()
bcf.allowedUnits = [.useMB] // optional: restricts the units to MB only
bcf.countStyle = .file
let string = bcf.string(fromByteCount: Int64(size.count))
print(string)
return string
} catch {
print("Failed to get size of store: \(error)")
return ""
}
}
guard let storeUrl = self.managedObjectContext!.persistentStoreCoordinator!.persistentStores.first?.url else {
print("There is no store url")
return
}
print("The size of the store is: \(self.getSqliteStoreSize(forPersistentContainerUrl: storeUrl))")
Reference: How to get the size of data present in coredata store?
Below is my coredata path.
file:///var/mobile/Containers/Data/Application/XXXXXX-XXX-XXXX-XXXX-XXXXXXXXXX/Library/Application Support/APPNAME.sqlite
Part of the answer is that you need to count the SQLite journal files. That's where most of the data actually lives. Getting a very low number is normal if you only count the central SQLite file. You find those by adding .-wal and -shm to file file name.
Another issue with your code is that using Data(contentsOf:) is a really bad idea here unless your store files are really small. You're reading the entire file into memory to find out how big it is, and that's not needed. The question you linked to has an answer showing how to use FileManager (actually NSFileManager there since it's ObjC but the function names and arguments are the same) to get the size without reading the file.
A final problem which might or might not affect you is that if you have external binary support turned on for any attribute in your model, the external files that get created are not in any of the SQLite files (that's the point). You also need to count those. The location is undocumented, but it's a directory named something like .APPNAME_SUPPORT/_EXTERNAL_DATA/. If you need this, try using a simulator build so that you can easily browse the app's files and make sure you have it right. You'd count every file in that folder.
By default, CoreData uses SQLite’s WAL mode (https://www.sqlite.org/wal.html) so SQLite creates 3 files for the database: the usual .sqlite file, a .sqlite-wal file, and a .sqlite-shm file. You will need to combine the sizes for all these.

Intermittent data loss with background fetch - could NSKeyedUnarchiver return nil from the documents directory?

I have a simple app that stores an array of my custom type (instances of a class called Drug) using NSCoding in the app’s documents folder.
The loading and saving code is an extension to my main view controller, which always exists once it is loaded.
Initialisation of array:
var drugs = [Drug]()
This array is then appended with the result of the loadDrugs() method below.
func saveDrugs() {
// Save to app container
let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(drugs, toFile: Drug.ArchiveURL.path)
// Save to shared container (for iMessage, Spotlight, widget)
let isSuccessfulSaveToSharedContainer = NSKeyedArchiver.archiveRootObject(drugs, toFile: Drug.SharedArchiveURL.path)
}
Here is the code for loading data.
func loadDrugs() -> [Drug]? {
var appContainerDrugs = NSKeyedUnarchiver.unarchiveObject(withFile: Drug.ArchiveURL.path) as? [Drug]
return appContainerDrugs
}
Data is also stored in iCloud using CloudKit and the app can respond to CK notifications to fetch changes from another device. Background fetch also triggers this same method.
// App Delegate
func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: #escaping (UIBackgroundFetchResult) -> Void) {
// Code to get reference to my main view controller
// This will have called loadDrugs() to populate local array drugs of type [Drug]
mainVC.getZoneChanges()
}
Finally, there is the getZoneChanges() method, which uses a stored CKServerChangeToken to get the changes from the private user database with CKFetchRecordZoneChangesOperation. The completion block calls saveDrugs().
The problem
All of this seems to work fine. However, sometimes all local data disappears between uses of the app, especially if it has not been used for some time. Deleting and reinstalling the app does pull the backed-up data from iCloud thankfully.
It seems to happen if the app has not been used for a while (presumably terminated by the system). Something has to have changed, so I presume it is the calling of a background fetch when the app is terminated that may be the problem. Everything works fine while debugging and when the app has been in foreground recently.
Possible causes
I’m guessing the problem is that I depend on background fetch (or receiving a CK notification) loading my main view controller in the background and then loading saved local data.
I have heard that UserDefaults does not work correctly in the background and there can be file security protections against accessing the documents directory in this context. If this is the case here, I could be loading an empty array (or rather initialising the array and not appending the data to it) and then saving it, overwriting existing data, all without the user knowing.
How can I circumvent this problem? Is there a way to check if the data is being loaded correctly? I tried making a conditional load with a fatal error if there is a problem, but this causes problems on the first run of the app as there is no data anyway!
Edit
The archive URLs are obtained dynamically as shown below. I just use a static method in my main data model class (Drug) to access them:
static let DocumentsDirectory = FileManager().urls(for: .documentDirectory, in: .userDomainMask).first!
static let ArchiveURL = DocumentsDirectory.appendingPathComponent("drugs")
The most common cause of this kind of issue is being awakened in the background when the device is locked and protected data are encrypted.
As a starting point, you can check UIApplication.isProtectedDataAvailable to verify that protected data is available. You can also lower the protection levels of data you require to .completeUntilFirstUserAuthentication (the specifics of how to do that depends on the how you create your files).
As a rule, you should minimize reducing data protection levels on sensitive information, so it's often best to write to some other location while the device is locked, and then merge that once the device is unlocked.
The problem is that you are storing the full path of the file and not just the fileName (or relative path). The document directory URL can change, and then if you stored that URL persistently you will not be pointing to the correct location of the file. Instead just store the filename and use NSFileManager URLsForDirectory to get the documents directly every time you need it.

Download in documents directory or move file async - iOS

I am building an iOS app in which the user can download different files.
I am using an URLSessionDownloadTask and an URLSession to download a file asynchronously.When the download is finished, the destination folder is by default, the tmp/ directory.
So, when the download ends, I need to move the temporary file to another directory.For a picture or a song, this takes only 1 second maybe even less. But when the file is a video for example, it can take up to 15 seconds.
The issue
To allow the user to still interact with the app, I would like to make this move asynchronous.Each time I try to do that, the file manager throws an exception.
“CFNetworkDownload_xxxxxx.tmp” couldn’t be moved to “Downloads” because either the former doesn't exist, or the folder containing the latter doesn't exist.
What have I tried
I tried to put the call to the file manager in a background thread, it throws.
I tried to remove the destination file before calling the move method, to make sure that the file doesn't already exists.
I tried to make a call to the copy function, before removing the file from the tmp/ directory.
My code
The call to the file manager looks like that.
func simpleMove(from location: URL, to dest: URL) -> Bool {
let fileManager = FileManager.default
do {
try fileManager.moveItem(at: location, to: dest)
return true
} catch {
print("\(error.localizedDescription)")
return false
}
}
When I put that in a background thread, I do it like that.
DispatchQueue.global().async {
if !simpleMove(from: location, to: dest) {
//Failure
}
}
Questions
How can I possibly move a really large file without affecting the UI?
It would be an even better solution to download the file directly in a permanent directory. How can I do that?
When I make the call to my simpleMove(from:to:) synchronously, it works perfectly.So, why the error says that the destination directory doesn't exists? (or something like that, I'm not sure of the meaning of that error)
Thanks.
Note
The code above is written in Swift 3, but if you have an Objective-C or a Swift 2 answer,feel free to share it as well!
Amusingly, the correct answer to this was posted in another question, where it was not the correct answer.
The solution is covered in Apple's Documentation where they state:
location
A file URL for the temporary file. Because the file is temporary, you must either open the file for reading or move it to a permanent location in your app’s sandbox container directory before returning from this delegate method.
If you choose to open the file for reading, you should do the actual reading in another thread to avoid blocking the delegate queue.
You are probably calling simpleMove from the success handler for the DownloadTask. When you call simpleMove on a background thread, the success handler returns and your temp file is cleaned up before simpleMove is even called.
The solution is to do as Apple says and open the file for reading:
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
do {
let file: FileHandle = try FileHandle(forReadingFrom: location)
DispatchQueue.global().async {
let data = file.readDataToEndOfFile()
FileManager().createFile(atPath: destination, contents: data, attributes: nil)
}
} catch {
// Handle error
}
}
I came across the same issue but i solve it.
First check that the file is exist in that path because i got issue because of the path extension are different of location URL. i was trying to rename audio but path extension was different(eg. mp3 to m4a)
Also in case there is any other file already exists at destination path
this issue arise.
So first try to check file exists at location where you by using
let fileManager = FileManager.default
if fileManager.fileExists(atPath: location.path) {
do {
try fileManager.moveItem(at: location, to: dest)
return true
} catch {
print("\(error.localizedDescription)")
return false
}
}
Hope this will help you

NSFileManager: Trying to delete files, but it says they don't exist

I'm trying to delete the files in my cache directory. However, when I tried to delete individual folders, I got an error saying the file doesn't exist, even though I know it does. So, I tried using a for loop to delete the all the files in the caches directory.
do {
for file in try NSFileManager.defaultManager().contentsOfDirectoryAtPath(cacheFolderPath) where !file.hasPrefix("."){
try NSFileManager.defaultManager().removeItemAtPath(file)
}
print("Cache cleared successfully.")
}
catch let error as NSError {
print(error.localizedDescription)
if let reason = error.localizedFailureReason {
print(reason)
}
}
}
However, this code prints this:
"CategoryThumbnails" couldn't be removed.
The file doesn't exist.
Well, it obviously does exist, as it was discovered by the contentsOfDirectoryAtPath method! How can it not exist? Does anybody know what's going on here, and how I can clear the cache?
The results of contentsOfDirectoryAtPath is a list of files in the given folder. But each of those results is not a complete path itself.
To delete each file, you need to append file to cacheFolderPath. Then pass that complete path to removeItemAtPath.
Another solution is to use one of the other folder enumeration methods of NSFileManager that return full URLs and let your provide options to skip hidden files.

spreadsheetgear - open/save file

I want to update an existing Excel file using SpreadsheetGear.
I load the Excel file using:
_activeWorkbook = Factory.GetWorkbook(#"./excel/test.xls");
workbookView1.ActiveWorkbook = _activeWorkbook;
but when I want to save it:
private void menuSave_Click(object sender, EventArgs e)
{
workbookView1.GetLock();
try
{
_activeWorkbook.Save();
}
finally
{
workbookView1.ReleaseLock();
}
}
I get this error: System.IO.IOException: The process cannot access the file 'C:...\bin\Debug\excel\test.xls' because it is being used by another process.
As the exception indicates, some other process (or possibly your same application, if you utilize multiple threads) has a lock on your file and so SpreadsheetGear cannot save it back to disk. Any number of other external processes could be the culprit, such as anti-virus software scanning your file or having it open in Excel itself when you try to save it. There is no way to determine the exact cause with just the information provided above.
What I can tell you is that SpreadsheetGear does not keep any file streams open after reading and writing files. The file stream and lock on the file is only open for as long as it takes to read/write its contents, which is usually very short if the file is small. Put another way, #".excel/test.xls" should be writable immediately after your Factory.GetWorkbook(...) line executes.

Resources