Skip to content

store ios: Exclude the database file from iOS backup#2195

Merged
gnprice merged 2 commits intozulip:mainfrom
rajveermalviya:pr-ios-backup
Mar 9, 2026
Merged

store ios: Exclude the database file from iOS backup#2195
gnprice merged 2 commits intozulip:mainfrom
rajveermalviya:pr-ios-backup

Conversation

@rajveermalviya
Copy link
Copy Markdown
Member

@rajveermalviya rajveermalviya commented Mar 3, 2026

Fixes #2158.

Tested by generating iOS backups from Finder app on macOS, and then using iTunes-Backup-Explorer to read the backup files:

Before After
Screenshot 2026-03-05 at 21 58 34 Screenshot 2026-03-05 at 21 58 01

@rajveermalviya rajveermalviya force-pushed the pr-ios-backup branch 2 times, most recently from 9012d25 to 0f3b485 Compare March 5, 2026 17:12
@rajveermalviya rajveermalviya marked this pull request as ready for review March 5, 2026 17:17
@rajveermalviya rajveermalviya added the maintainer review PR ready for review by Zulip maintainers label Mar 5, 2026
Copy link
Copy Markdown
Collaborator

@chrisbobbe chrisbobbe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Comments below.

Comment on lines -71 to +83
37D847A22F4CE7C20020EB99 /* RunnerUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = RunnerUITests; sourceTree = "<group>"; };
37D847A22F4CE7C20020EB99 /* RunnerUITests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
explicitFileTypes = {
};
explicitFolders = (
);
path = RunnerUITests;
sourceTree = "<group>";
};
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The substantive change here is just to add an empty exceptions entry, right? It looks like this section was added in 509d88f (Patrol setup); I suspect it's not related to the intended change for your commit.

Would you take a few minutes to try and reproduce this change on its own, and either discard it or make a separate commit for it depending on what you learn? (E.g. probably we want to keep this change if it happens naturally when you clear an Xcode cache and rebuild, because in that case it probably belonged in 509d88f and was just left out accidentally.)

Comment on lines +1121 to +1124
if (defaultTargetPlatform == TargetPlatform.iOS) {
// Exclude the database file from backups on iOS.
await IosNativeHostApi().setExcludedFromBackup(file.path);
}
Copy link
Copy Markdown
Collaborator

@chrisbobbe chrisbobbe Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this throws it'll block loading the app (i.e. stop before creating the LiveGlobalStore) with no feedback to the user. Maybe instead run this unawaited() with a TODO(log) on error?

Also how about putting this call immediately after the await _dbFile() line? It sets an attribute on the file; the intervening database queries (await db.getGlobalSettings() etc.) seem unrelated and not prerequisites for this filesystem action.

That proximity should also help us remember to handle this detail carefully in the migration in your other PR #2156.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't put this after _dbFile(), because the database file may not exists yet. We can though put it after AppDatabase(NativeDatabase.createInBackground(file));, moved there.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm but will the file exist right after the AppDatabase(NativeDatabase.createInBackground(file)) line? There's no await there, so maybe that line creates the file synchronously…which seems unusual? But then why is the method's name createInBackground?

I wonder if, in your previous revision, await db.getGlobalSettings() and friends were actually giving time for the file to finish being created asynchronously, before the setExcludedFromBackup call? Causing that call to succeed basically by accident.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A call like await db.getGlobalSettings() will wait for the database to be opened, which I believe will involve creating the database file if it doesn't exist. (Ultimately it calls sqlite3_open_v2 with flags SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, via [Sqlite3Implementation.open].)

I just spent a few minutes looking around Drift's API, and I don't see a totally clean way to say "wait for the database to be opened, then do X" short of actually making a query. (The closest is await db.executor.ensureOpen(db); but the type of executor is documented to be internal to Drift and unstable.)

Copy link
Copy Markdown
Member Author

@rajveermalviya rajveermalviya Mar 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NativeDatabase.createInBackground(file) returns the QueryExecutor which is same as db.executor. So, AIUI it should be safe to use executor.ensureOpen via the returned QueryExecutor?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, there's db.doWhenOpened but it also says:

Calling this method directly might circumvent the current transaction. For that reason, it should only be called inside drift.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted to the previous revision for now, where the call is made at the end of the load method.

notificationTapEventListener = NotificationTapEventListener()
NotificationTapEventsStreamHandler.register(with: controller.binaryMessenger, streamHandler: notificationTapEventListener!)

IosNativeHostApiSetup.setUp(binaryMessenger: controller.binaryMessenger, api: IosNativeHostApiImpl())
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: how about moving this above the // Retrieve the remote notification payload comment, so it doesn't interrupt notification-related code


@HostApi()
abstract class IosNativeHostApi {
/// Sets the flag to exclude from iOS backups for the given filepath.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can be more transparent about what this method does:

Suggested change
/// Sets the flag to exclude from iOS backups for the given filepath.
/// Sets UrlResourceValues.isExcludedFromBackup for the given file path.
///
/// See doc:
/// https://developer.apple.com/documentation/foundation/urlresourcevalues/isexcludedfrombackup

var resourceValues = URLResourceValues()
resourceValues.isExcludedFromBackup = true

var url = URL(fileURLWithPath: path)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var url = URL(fileURLWithPath: path)
var url = URL(fileURLWithPath: path, isDirectory: false)

I think? In case this affects app startup time; see doc:

https://developer.apple.com/documentation/foundation/improving-performance-and-stability-when-accessing-the-file-system#Provide-directory-hints-for-file-URLs-to-avoid-wasteful-IO

For earlier versions [than iOS 16], opt for the nonblocking variants of the methods in the following table.

@rajveermalviya rajveermalviya force-pushed the pr-ios-backup branch 2 times, most recently from 6fe5c64 to b91fe25 Compare March 6, 2026 15:19
@rajveermalviya
Copy link
Copy Markdown
Member Author

Thanks for the review @chrisbobbe! Pushed an update, PTAL.

Copy link
Copy Markdown
Collaborator

@chrisbobbe chrisbobbe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!! Comments below.

Comment on lines +28 to +30

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: indentation

Comment on lines +1121 to +1124
if (defaultTargetPlatform == TargetPlatform.iOS) {
// Exclude the database file from backups on iOS.
await IosNativeHostApi().setExcludedFromBackup(file.path);
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm but will the file exist right after the AppDatabase(NativeDatabase.createInBackground(file)) line? There's no await there, so maybe that line creates the file synchronously…which seems unusual? But then why is the method's name createInBackground?

I wonder if, in your previous revision, await db.getGlobalSettings() and friends were actually giving time for the file to finish being created asynchronously, before the setExcludedFromBackup call? Causing that call to succeed basically by accident.

@rajveermalviya rajveermalviya force-pushed the pr-ios-backup branch 2 times, most recently from 8c8c0a9 to e85346a Compare March 9, 2026 04:57
@rajveermalviya
Copy link
Copy Markdown
Member Author

Thanks for the review @chrisbobbe! Pushed an update, PTAL.

Copy link
Copy Markdown
Collaborator

@chrisbobbe chrisbobbe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Bump on something small below, and marking for Greg's review.

Comment on lines +1121 to +1123
// Disable OS backups for the database file, see:
// https://github.com/zulip/zulip-flutter/issues/2158
unawaited(_maybeDisableOsBackup(file));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Disable OS backups for the database file, see:
// https://github.com/zulip/zulip-flutter/issues/2158
unawaited(_maybeDisableOsBackup(file));
// Disable OS backups for the database file, see:
// https://github.com/zulip/zulip-flutter/issues/2158
unawaited(_maybeDisableOsBackup(file)); // TODO(log) on error

@chrisbobbe chrisbobbe requested a review from gnprice March 9, 2026 17:15
@chrisbobbe chrisbobbe assigned gnprice and unassigned chrisbobbe Mar 9, 2026
@chrisbobbe chrisbobbe added integration review Added by maintainers when PR may be ready for integration and removed maintainer review PR ready for review by Zulip maintainers labels Mar 9, 2026
Just running `flutter build ios --config-only` wasn't sufficient,
there were no changes, I had to trigger Xcode's edits to the
`project.pbxproj` first, by changing the order of build phases
(moved "Compile Sources" phase above "Run Script") and then again
reverted the order of those build phases.  The Xcode's edits ended
up being changing the `objectVersion = 54;` to
`objectVersion = 70;`, and addition of couple of empty
`inputPaths = ();` and `outputPaths = ();`.

After Xcode's edits to `project.pbxproj`, I ran
`flutter build ios --config-only` which resulted in this commit.

This tells that this was probably missed in merge/rebase conflict
in 509d88f (Patrol setup). Which changed the `objectVersion`
entry to `objectVersion = 54;` but missed the change to
`37D847A22F4CE7C20020EB99 /* RunnerUITests */ = {` block. So, on
the next `flutter run/build`, it probably doesn't bother to verify
or make changes to the `project.pbxproj` because `objectVersion` is
already set to a value that `flutter run/build` likes.
@gnprice
Copy link
Copy Markdown
Member

gnprice commented Mar 9, 2026

Thanks @rajveermalviya for taking care of this, and @chrisbobbe for the detailed reviews!

Looks good; merging, with a few added comments including the one Chris suggested above:

diff --git lib/model/store.dart lib/model/store.dart
index 7c8fe3ecd..1084a2f01 100644
--- lib/model/store.dart
+++ lib/model/store.dart
@@ -1120,7 +1120,9 @@ class LiveGlobalStore extends GlobalStore {
 
     // Disable OS backups for the database file, see:
     //   https://github.com/zulip/zulip-flutter/issues/2158
-    unawaited(_maybeDisableOsBackup(file));
+    // This comes after the queries above, because it must come after
+    // the database file has been created on disk.
+    unawaited(_maybeDisableOsBackup(file)); // TODO(log) on error
 
     return LiveGlobalStore._(
       backend: LiveGlobalStoreBackend._(db: db),
diff --git pigeon/ios_native.dart pigeon/ios_native.dart
index f33261120..40bbebe8d 100644
--- pigeon/ios_native.dart
+++ pigeon/ios_native.dart
@@ -12,6 +12,9 @@ import 'package:pigeon/pigeon.dart';
 abstract class IosNativeHostApi {
   /// Sets UrlResourceValues.isExcludedFromBackup for the given file path.
   ///
+  /// The file at this path must already exist,
+  /// and be a regular file (not a directory).
+  ///
   /// See doc:
   ///   https://developer.apple.com/documentation/foundation/urlresourcevalues/isexcludedfrombackup
   ///   https://developer.apple.com/documentation/foundation/optimizing-your-app-s-data-for-icloud-backup

Those two points seemed like things that would otherwise be easy to miss when making future edits to this code.

@gnprice gnprice merged commit 1d03e57 into zulip:main Mar 9, 2026
1 of 2 checks passed
gnprice added a commit to gnprice/zulip-flutter that referenced this pull request Mar 9, 2026
These changes belonged in 1d03e57.  Before merging that PR zulip#2195,
I edited this comment in the source file of the pigeon but didn't
rerun generating the files (nor CI), oops.
Rofumer pushed a commit to Rofumer/zulip-flutter that referenced this pull request Mar 24, 2026
These changes belonged in 19d67b4.  Before merging that PR zulip#2195,
I edited this comment in the source file of the pigeon but didn't
rerun generating the files (nor CI), oops.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integration review Added by maintainers when PR may be ready for integration

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Exclude keys and credentials from device backups

3 participants