How do you allow very large files to have time to upload to firebase before iOS terminates the task? - ios

I have a video sharing app, and when you save a video to firebase storage it works perfectly for videos that are roughly 1 minute or shorter.
The problem that I am having, is when I try to post a longer video (1 min or greater) it never saves to firebase.
The only thing that I can think of is this error that I am getting, and this error only shows up about 30 seconds after I click the save button:
[BackgroundTask] Background Task 101 ("GTMSessionFetcher-firebasestorage.googleapis.com"), was created over 30 seconds ago. In applications running in the background, this creates a risk of termination. Remember to call UIApplication.endBackgroundTask(_:) for your task in a timely manner to avoid this.
Here is my code to save the video to firebase.
func saveMovie(path: String, file: String, url: URL) {
var backgroundTaskID: UIBackgroundTaskIdentifier?
// Perform the task on a background queue.
DispatchQueue.global().async {
// Request the task asseration and save the ID
backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "Finish doing this task", expirationHandler: {
// End the task if time expires
UIApplication.shared.endBackgroundTask(backgroundTaskID!)
backgroundTaskID = UIBackgroundTaskIdentifier.invalid
})
// Send the data synchronously
do {
let movieData = try Data(contentsOf: url)
self.storage.child(path).child("\(file).m4v").putData(movieData)
} catch let error {
fatalError("Error saving movie in saveMovie func. \(error.localizedDescription)")
}
//End the task assertion
UIApplication.shared.endBackgroundTask(backgroundTaskID!)
backgroundTaskID = UIBackgroundTaskIdentifier.invalid
}
}
Any suggestions on how I can allow my video time to upload?

Finally figured this out after a long time...
All you have to do is use .putFile("FileURL") instead of .putdata("Data"). Firebase documentation says you should use putFile() instead of putData() when uploading large files.
But the hard part is for some reason you can't directly upload the movie URL that you get from the didFinishPickingMediaWithInfo function and firebase will just give you an error. So what I did instead was get the data of the movie, save the movie data to a path in the file manager, and use the file manager path URL to upload directly to firebase which worked for me.
//Save movie to Firestore
do {
// Convert movie to Data.
let movieData = try Data(contentsOf: movie)
// Get path so we can save movieData into fileManager and upload to firebase because movie URL does not work, but fileManager url does work.
guard let path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.appendingPathComponent(postId!) else { print("Error saving to file manager in addPost func"); return }
do {
try movieData.write(to: path)
// Save the file manager url file to firebase storage
Storage.storage().reference().child("Videos").child("\(postId!).m4v").putFile(from: path, metadata: nil) { metadata, error in
if let error = error {
print("There was an error \(error.localizedDescription)")
} else {
print("Video successfully uploaded.")
}
// Delete video from filemanager because it would take up too much space to save all videos to file manager.
do {
try FileManager.default.removeItem(atPath: path.path)
} catch let error {
print("Error deleting from file manager in addPost func \(error.localizedDescription)")
}
}
} catch let error {
print("Error writing movieData to firebase \(error.localizedDescription)")
}
} catch let error {
print("There was an error adding video in addPost func \(error.localizedDescription)")
}

Related

Realm iOS: How to handle Client Reset

Basically, I want to handle a case where any device got SyncError with type ClientResetError then, want my device to re-login to realm again. but as per documentation, we have to closeRealmSafely before I login to realm again, but I am not sure how to close realm safely.
I am going through the doc (https://docs.realm.io/sync/using-synced-realms/errors#client-reset) to handle client reset error and found it's very confusing . I want help to understand about the following code.
First there is no method available to closeRealmsafely. Please help me understand how can I close the realm safely?
How can I backup and when I will use it? Should I skip the reset error because in documentation it's mentions if the client reset process is not manually initiated, it will instead automatically take place after the next time the app is launched, upon first accessing the SyncManager singleton. It is the app’s responsibility to persist the location of the backup copy if needed, so that the backup copy can be found later."
Below is the error handler sample code from the doc.
let syncError = error as! SyncError
switch syncError.code {
case .clientResetError:
if let (path, clientResetToken) = syncError.clientResetInfo() {
closeRealmSafely()
saveBackupRealmPath(path)
SyncSession.immediatelyHandleError(clientResetToken)
}
default:
// Handle other errors...
()
}
}```
Finally we figured out how to handle the client reset error. We have taken following steps To avoid the data loss incase user is offline and came online and got reset error.
Save the local realm to another directory
Invalidate and nil the realm
Initiate realm manual reset - Call SyncSession.immediatelyHandleError with clientResetToken passed and it will delete the existing realm from directory
Show client reset alert - This will intimate user to relaunch the app.
On next launch realm creates a fresh realm from ROS.
After new realm connects, restore the realm records (if any) from the old realm saved in backup directory above.
Delete the backup realm(old realm) from directory.
switch syncError.code {
case .clientResetError:
if let (path, clientResetToken) = syncError.clientResetInfo() {
// taking backup
backUpRealm(realm: yourLocalRealm)
// making realm nil and invalidating
yourLocalRealm?.invalidate()
yourLocalRealm = nil
//Initiate realm manual reset - Call `SyncSession.immediatelyHandleError` with `clientResetToken` passed and it will delete the existing realm from directory
SyncSession.immediatelyHandleError(clientResetToken)
// can show alert to user to relaunch the app
showAlertforAppRelaunch()
}
default:
// Handle other errors...
()
}
}```
The back up realm code look like this:
func backUpRealm(realm: Realm?) {
do {
try realm?.writeCopy(toFile: backupUrl)
} catch {
print("Error backing up data")
}
}
After doing this backup will be available at backup path. On next launch device will connect and download a fresh realm from ROS so after device connects restore the realm records from the backup realm saved in the backup path.
The restore merge backup code will look like this. place the below method when realm connects after relauch.The ```restoredRealm`` is fresh downloaded realm on launch
func restoreAndMergeFromBackup(restoredRealm: Realm?) {
let realmBackUpFilePath = isRealmBackupExits()
// check if backup exists or not
if realmBackUpFilePath.exists {
let config = Realm.Configuration(
fileURL: URL(fileURLWithPath: realmBackUpFilePath.path),
readOnly: true)
let realm = try? Realm(configuration: config)
guard let backupRealm = realm else { return }
//Get your realm Objects
let objects = backupRealm.objects(YourRealmObject.self)
try? restoredRealm?.safeWrite {
for object in objects {
// taking local changes to the downloaded realm if it has
restoredRealm?.create(YourRealmObject.self, value: object, update: .modified)
}
self.removeRealmFiles(path: realmBackUpFilePath.path)
}
} else {
debug("backup realm does not exists")
}
}
private func isRealmBackupExits() -> (exists: Bool, path: String) {
let documentsPath = URL(fileURLWithPath: NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0])
let realmPathComponent = documentsPath.appendingPathComponent("your_backup.realm")
let filePath = realmPathComponent.path
let fileManager = FileManager.default
if fileManager.fileExists(atPath: filePath) {
return (true, filePath)
}
return (false, "")
}
private func removeRealmFiles(path: String) {
let realmURL = URL(fileURLWithPath: path)
let realmURLs = [
realmURL,
realmURL.appendingPathExtension("lock"),
realmURL.appendingPathExtension("realm"),
realmURL.appendingPathExtension("management")
]
for URL in realmURLs {
do {
try FileManager.default.removeItem(at: URL)
} catch {
debug("error while deleting realm urls")
}
}
}```
In our testing we have found that there is a backup made by realm automatically so we deleted it for safety purpose. the path argument you will get in the if let (path, clientResetToken) = syncError.clientResetInfo()
func removeAutoGeneratedRealmBackUp(path: String) {
do {
try FileManager.default.removeItem(at: URL(fileURLWithPath: path))
} catch {
debug("error while deleting realm backUp path \(path)")
}
}

Unable to destroy persistent store created with Core Data and SQLite

I have an iOS app where I want to start with a fresh Core Data database on every launch. The store type is SQLite.
However, when I call persistentStoreCoordinator.destroyPersistentStore(), I get an error 100% of the time.
Here is the code:
func destroyPersistentStore() {
guard let modelURL = Bundle.main.url(forResource: self.modelName, withExtension: "momd") else {
print("Missing data model - could not destroy")
return
}
do {
try persistentStoreCoordinator.destroyPersistentStore(at: modelURL, ofType: storeType, options: nil)
} catch {
print("Unable to destroy persistent store: \(error) - \(error.localizedDescription)")
}
}
The error is:
Unable to destroy persistent store: Error Domain=NSSQLiteErrorDomain
Code=14 "(null)" UserInfo={NSFilePath=.../AppName.app/ModelName.momd,
reason=Failed to truncate database} - The operation couldn’t be
completed. (NSSQLiteErrorDomain error 14.)
Even after this error, the app is able to save and access data in the store. The problem is that the initial data is being loaded on each launch, creating duplicates.
Here is the situation at the point where the call to destroyPersistentStore takes place:
The SQLite data file definitely exists and contains data
Happens on simulator or real device
The modelUrl is correct and points to the momd
Store type is SQLite
SQLite data file is saved in Documents directory
persistentStoreCoordinator.url(for: persistentStoreCoordinator.persistentStores.first!) is pointing to the file in the Documents directory.
I've searched online for answers and can't find anyone reporting this error, but I have the error in both this project and a simplified demo project. I cannot make destroyPersistentStore work at all.
Lastly, when I pause execution and po the persistentStoreCoordinator.managedObjectModel, the first line is:
po persistentStoreCoordinator.managedObjectModel
() isEditable 0, entities...
Could the isEditable issue be the problem? How would I change it?
You're conflating two objects in the Core Data stack:
The model is inside your app bundle, has the extension .momd, and contains information about your Core Data object definitions: what entities you have, what properties they have, their relationships, and so on.
The persistent store is a data file in your app's container (not in the bundle). You define its URL when you create or load persistent stores. It contains data for actual instances of model objects, rather than abstract definitions.
Rather than getting the URL of your model, I think you want to get the URL of a persistent store. You can do that by looking at the persistent store coordinator's persistentStores array, picking one, and getting its URL:
func destroyPersistentStore() {
guard let firstStoreURL = persistentStoreCoordinator.persistentStores.first?.url else {
print("Missing first store URL - could not destroy")
return
}
do {
try persistentStoreCoordinator.destroyPersistentStore(at: firstStoreURL, ofType: storeType, options: nil)
} catch {
print("Unable to destroy persistent store: \(error) - \(error.localizedDescription)")
}
}
This would destroy the first store; if you have multiple, you could instead loop over the persistent stores destroying them all, depending on your app's requirements.
iOS 15 version
// function to delete persistent store
func deletePersistentStore() {
let coordinator = self.persistentContainer.persistentStoreCoordinator
guard let store = coordinator.persistentStores.first else {
return
}
let storeURL = coordinator.url(for: store)
do {
if #available(iOS 15.0, *) {
let storeType: NSPersistentStore.StoreType = inMemoryStore ? .inMemory : .sqlite
try coordinator.destroyPersistentStore(at: storeURL, type: storeType)
} else {
let storeType: String = inMemoryStore ? NSInMemoryStoreType : NSSQLiteStoreType
try coordinator.destroyPersistentStore(at: storeURL, ofType: storeType)
}
}
catch {
print(error.localizedDescription)
}
}
Found this great solution:
import Foundation
import CoreData
extension NSManagedObjectContext
{
func deleteAllData()
{
guard let persistentStore = persistentStoreCoordinator?.persistentStores.last else {
return
}
guard let url = persistentStoreCoordinator?.url(for: persistentStore) else {
return
}
performAndWait { () -> Void in
self.reset()
do
{
try self.persistentStoreCoordinator?.remove(persistentStore)
try FileManager.default.removeItem(at: url)
try self.persistentStoreCoordinator?.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil)
}
catch { /*dealing with errors up to the usage*/ }
}
}
}

Swift File Download Issue

I am trying to download a plist file from a remote location and use it in the iOS app I am creating. The file is going to be used for calendar details within the app's calendar. The goal is obviously that I can update the remote file instead of having to push updates to the app itself every time we need to make changes to calendar details.
I started with the code used in this example: Download File From A Remote URL
Here is my modified version:
// Create destination URL
let documentsUrl:URL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first as URL!
let destinationFileUrl = documentsUrl.appendingPathComponent("2017.plist")
//let destinationFileUrl = URL(string: Bundle.main.path(forResource: String(currentYear), ofType: "plist")!)
//Create URL to the source file you want to download
let fileURL = URL(string: "https://drive.google.com/open?id=0BwHDQFwaL9DuLThNYWwtQ1VXblk")
let sessionConfig = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfig)
let request = URLRequest(url:fileURL!)
let task = session.downloadTask(with: request) { (tempLocalUrl, response, error) in
if let tempLocalUrl = tempLocalUrl, error == nil {
// Success
if let statusCode = (response as? HTTPURLResponse)?.statusCode {
print("Successfully downloaded. Status code: \(statusCode)")
}
do {
try FileManager.default.removeItem(at: destinationFileUrl)
try FileManager.default.moveItem(at: tempLocalUrl, to: destinationFileUrl)
print("File was replaced")
print(NSArray(contentsOf: tempLocalUrl))
//print(tempLocalUrl)
} catch (let writeError) {
print("Error creating a file \(String(describing: destinationFileUrl)) : \(writeError)")
}
} else {
print("Error took place while downloading a file. Error description: %#", error?.localizedDescription as Any);
}
}
task.resume()
I originally tried to overwrite the file that is bundled with the app to being with, that resulted in errors. So I instead tried to just save it in the app's documents folder and that removed that error. I had to make sure and remove any previous version of the file because it was giving me a file already exists error after the first run.
While it says everything is working (The outputs for both successful download and replaced file happen) when I print the contents of the array from the downloaded URL it just gives me nil.
This is my first attempt to use any kind of external resources in an app. Before I have always kept everything internal, so I am sure there is something glaringly obvious I am missing.
Update 1:
I realized I didn't have the correct URL to use to download a file from a Google drive. That line of code has been changed to:
let fileURL = URL(string: "https://drive.google.com/uc?export=download&id=0BwHDQFwaL9DuLThNYWwtQ1VXblk")
So now I actually am downloading the plist like I originally thought I was. Even removing the deletion issue mentioned in the first comment, I still can't get the downloaded file to actually replace the existing one.
Update 2:
I have reduced the actual file manipulation down to the following:
do {
try FileManager.default.replaceItemAt(destinationFileUrl, withItemAt: tempLocalUrl)
print("File was replaced")
print(NSArray(contentsOf: destinationFileUrl))
} catch (let writeError) {
print("Error creating a file \(String(describing: destinationFileUrl)) : \(writeError)")
}
} else {
print("Error took place while downloading a file. Error description: %#", error?.localizedDescription as Any);
}
After the replacement is performed the output of the file shows the correct new contents that were downloaded from the internet.
Later in the code when I try and access the file it seems to be nil in content again.
Look at your download completion code. You:
Delete the file at the destination URL (in case there was one
leftover)
MOVE the temp file to the destination URL (removing it from the temp
URL)
Try to load the file from the temp URL.
What's wrong with this picture?
You are trying to get the contents of the moved file. You already moved the file to destination url and then you are trying to get the contents of the file from temporary location.
For getting file data, Please try the following :
let fileData = try! String(contentsOf: destinationFileUrl, encoding: String.Encoding.utf8)
print(fileData)

Unable save large file to S3 using Parse server

I tried to save to S3 bucket using Parse Server, and it can be saved correctly when the file is small, such as 864.2KB. However, when the file is large, say 5MB, it complaints with a message saying: "The data couldn’t be read because it isn’t in the correct format"
I'm using the following code to save the the video file to the S3
func saveVideo(withVideoURL url: URL){
let post = PFObject(className: "Post")
post["caption"] = "Out of the game for 6 months, but back with vengeance. Meet your 2017 AO Men's champion"
do{
let data = try Data(contentsOf: url)
print(data)
post["media"] = PFFile(data: data)
post.saveInBackground { (success, error) in
if success{
print("video saved")
}else{
print("failed")
if error != nil{
print(error!.localizedDescription)
}else{
print("erorr is nil")
}
}
}
}catch let error as NSError{
print("can't read")
print(error.localizedDescription)
}
}
Besides, even when the small video file is indeed being saved to the S3, it contains an extension .bin instead of, for example .mp4. I wonder what's happening here
The url end up looking something like this
https://s3-us-west-1.amazonaws.com/sampleApp/19d5bce20f8b55te1b1b8f370212533e5_file.bin
You need to stipulate the content type. You can do so like this:
post["media"] = PFFile(data: data, contentType: "video/mp4")
The below settings in your parse-server index file will help you:
var bodyParser = require('body-parser');
app.use(bodyParser.json({limit: '20mb'}));
app.use(bodyParser.urlencoded({limit: '20mb', extended: true}));
If you are using elastic beanstalk, you have to have a file named files.config inside the folder .ebextensions, with the below content.
files:
/etc/nginx/conf.d/proxy.conf:
content: |
client_max_body_size 20M;
This fixed the issue for me.

Firebase Storage: child() doesn't work with iOS App

I'm getting the following error when trying to download an image from my Firebase Storage:
Error Domain=FIRStorageErrorDomain Code=-13010 "Object 2xxxxxxx8/profile_pic does not exist."
(I obviously put the x's up there to mask private info.)
I'm adding a path reference to my Firebase Storage using the following code:
let storage = FIRStorage.storage()
let storageRef = storage.referenceForURL("gs://project-4xxxxxxxxxxxxx.appspot.com")
let profilePicReference = storageRef.child(signedInUser.uid + "/profile_pic")
I know the code above is good cause everything was working correctly: I could see a folder was added in my Storage space, and an image was uploaded into that folder - all directly from my iOS App.
The problems started when I manually deleted said folder from my Firebase Storage (I did this through the Firebase web portal) - just cause I wanted verify everything was working, so I deleted the folder to start fresh - expecting the code above would recreate it once I ran the App again - and since then I'm getting this error over and over again.
Really makes no sense.
Are there any quirks or issues with Firebase Storage? Some sort of caching that has to be addressed?
Any tips would be greatly appreciated!
Are there any quirks or issues with Firebase Storage? Some sort of
caching that has to be addressed?
An UploadTask executes asynchronously. If I try downloading an image immediately after uploading an image, I can reproduce your error. What's happening is that the download code executes before the image finishes uploading, producing the image-does-not-exist error. You can see that the download code executes too early by printing out some messages in the callbacks:
let storage = FIRStorage.storage()
let storageRef = storage.reference() //You don't need to explicitly write the url in your code.
//The config file GoogleService-Info.plist will handle that.
let imageRef = storageRef.child("images/align_menu.tiff")
let localURL = NSBundle.mainBundle().URLForResource(
"align_menu",
withExtension: "tiff"
)!
//Upload the image:
let uploadTask = imageRef.putFile(localURL, metadata: nil) { (metadata, error) -> Void in
if let returnedError = error {
// Uh-oh, an error occurred!
print("[My Upload Error]: \(returnedError)")
} else {
// Metadata contains file metadata such as size, content-type, and download URL.
print("[My Upload Success]:")
let downloadURL = metadata!.downloadURL()!
print("[URL for download]: \(downloadURL)")
}
}
//Download the image:
imageRef.dataWithMaxSize(1 * 1024 * 1024) { (data, error) -> Void in
if let returnedError = error {
// Uh-oh, an error occurred!
print("[My Download Error]: \(returnedError)")
}
else {
print("[My Download Success]:")
if let validImage = UIImage(data: data!) {
NSOperationQueue.mainQueue().addOperationWithBlock() {
self.imageView.image = validImage
}
}
}
}
That code produces the output:
[My Download Error]: ...."Object images/align_menu.tiff does not exist."...
and then after a few seconds I see the output:
[My Upload Success]:
[URL for download]: ...
which demonstrates that the download callback is executing before the upload callback. I can't quite figure out the details of why that happens--but obviously the callbacks are not added to a serial queue.*
To cure the asynchronous problem, you have several options:
1) Put the download code inside the callback for the upload code.
That way, the download won't start executing until after the image has successfully uploaded. After I did that, deleting the image using the Firebase Storage webpage before running the app had no deleterious effect on my upload/download, and the messages were output in the expected order:
[My Upload Success]:
[URL for download]: ...
[My Download Success]:
2) Attach a .Success observer to the uploadTask.
As described in the Firebase docs, in the Monitor Upload Progress section, you can get notified if the uploadTask successfully uploads the image:
let storage = FIRStorage.storage()
let storageRef = storage.reference() //You don't need to explicitly write the url in your code.
//The config file GoogleService-Info.plist will handle that.
let imageRef = storageRef.child("images/align_menu.tiff")
let localURL = NSBundle.mainBundle().URLForResource(
"align_menu",
withExtension: "tiff"
)!
//Upload the image:
let uploadTask = imageRef.putFile(localURL, metadata: nil) { (metadata, error) -> Void in
if let returnedError = error {
// Uh-oh, an error occurred!
print("[My Upload Error]: \(returnedError)")
} else {
// Metadata contains file metadata such as size, content-type, and download URL.
print("[My Upload Success]:")
let downloadURL = metadata!.downloadURL()!
print("[URL for download]: \(downloadURL)")
}
}
let observer = uploadTask.observeStatus(.Success) { (snapshot) -> Void in
//Download the image:
imageRef.dataWithMaxSize(1 * 1024 * 1024) { (data, error) -> Void in
if let returnedError = error {
// Uh-oh, an error occurred!
print("[My Download Error]: \(returnedError)")
}
else {
print("[My Download Success]:")
if let validImage = UIImage(data: data!) {
NSOperationQueue.mainQueue().addOperationWithBlock() {
self.imageView.image = validImage
}
}
}
}
}
3) Use Grand Central Dispatch to notify you when the upload is successful.
You don't have control over what queues the callbacks get added to (the Firebase method implementations decide that), but you can use Grand Central Dispatch to notify you when arbitrary code finishes executing. The following works for me:
let storage = FIRStorage.storage()
let storageRef = storage.reference() //You don't need to explicitly write the url in your code.
//The config file GoogleService-Info.plist will handle that.
let imageRef = storageRef.child("images/align_menu.tiff")
let localURL = NSBundle.mainBundle().URLForResource(
"align_menu",
withExtension: "tiff"
)!
let myExecutionGroup = dispatch_group_create()
dispatch_group_enter(myExecutionGroup)
//Upload the image:
let _ = imageRef.putFile(localURL, metadata: nil) { (metadata, error) -> Void in
if let returnedError = error {
// Uh-oh, an error occurred!
print("[My Upload Error]: \(returnedError)")
} else {
// Metadata contains file metadata such as size, content-type, and download URL.
print("[My Upload Success]:")
let downloadURL = metadata!.downloadURL()!
print("[URL for download]: \(downloadURL)")
dispatch_group_leave(myExecutionGroup)
}
}
let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)
dispatch_group_notify(myExecutionGroup, queue) {
//This callback executes for every dispatch_group_leave().
//Download the image:
imageRef.dataWithMaxSize(1 * 1024 * 1024) { (data, error) -> Void in
if let returnedError = error {
// Uh-oh, an error occurred!
print("[My Download Error]: \(returnedError)")
}
else {
print("[My Download Success]:")
if let validImage = UIImage(data: data!) {
NSOperationQueue.mainQueue().addOperationWithBlock() {
self.imageView.image = validImage
}
}
}
}
}
* I tried putting a sleep(10) between the original upload code and download code, and that did not alleviate the problem. I thought that if the upload callback was executing on a background thread, then the upload callback would have time to complete while the main thread was sleeping, then after the sleep finished the download code would execute and the download callback would be added to a queue somewhere, then the download callback would execute. Because the sleep(10) didn't solve the problem, that meant the upload callback had to have been added to an execution queue for the main thread, and the sleep halted the main thread and anything in the queue from executing.
That leads me to believe that the upload and download callbacks are added to an asynchronous queue on the main thread (it's not a synchronous queue otherwise the callbacks would execute in order). I think an asynchronous queue on the main thread means that when there is dead time on the main thread, the tasks in the queue will execute, and you also get rapid switching between the various tasks when there is dead time in a particular task, like waiting for an HTTP response. For example, if there are two tasks in an asynchronous queue on the main thread, then there is rapid switching between the main thread, task1, and task2 whenever there is dead time in any one of them.

Resources