diff --git a/CHANGELOG.md b/CHANGELOG.md index 82203c1597..074014359a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ * None ### Enhancements -* Added `excludeFromIcloudBackup` option to the `Realm` constructor to exclude the realm files from iCloud backup. ([#4139](https://github.com/realm/realm-js/issues/4139)) +* Added `excludeFromIcloudBackup` option to the `Realm` constructor to exclude the realm files from iCloud backup. ([#4139](https://github.com/realm/realm-js/issues/4139) and [#6927](https://github.com/realm/realm-js/pull/6927)) ```typescript const realm = new Realm({ schema: [ diff --git a/contrib/guide-testing-exclude-icloud-backup.md b/contrib/guide-testing-exclude-icloud-backup.md index fe57cddff1..a5d24c4ad2 100644 --- a/contrib/guide-testing-exclude-icloud-backup.md +++ b/contrib/guide-testing-exclude-icloud-backup.md @@ -1,5 +1,29 @@ # Guide: Testing Exclude iCloud Backup +## Prerequisites + +- macOS +- iOS Simulator + +## Testing + +Ensure you have booted a simulator and execute the integration tests on iOS: + +```sh +MOCHA_REMOTE_GREP='icloud' npm run test:ios --workspace @realm/react-native-test-app-tests +``` + +In the command above, we're explicitly grepping for the icloud backup tests. + +To verify if a file has been successfully excluded from iCloud backup, you need to check the file's attributes. +We provide an easy script to do so: + +```sh +./contrib/scripts/check-exclude-icloud-backup.sh +``` + +## Testing in your own app + Before starting the testing process, you need to configure your Realm database to either include or exclude files from iCloud backup. This is done by setting the `excludeFromIcloudBackup` property in your Realm configuration. Here is an example of how to set this property: ```javascript @@ -16,20 +40,7 @@ const realm = new Realm(realmConfig); Make sure to replace the schema and path with your actual Realm schema and desired file path. Once you have configured this property, you can proceed with the following steps to test if the exclusion from iCloud backup is working correctly. -## Prerequisites - -- macOS -- iOS Simulator - -## Testing - -To verify if a file has been successfully excluded from iCloud backup, you need to check the file's attributes. We provide an easy script to do so. Ensure you have booted a simulator with an app using Realm. From the root of the project, run: - -```sh -contrib/scripts/check-exclude-icloud-backup.sh -``` - -If the script doesn't work, you can also check it manually. First, get the path of the Realm files from the simulator's Documents folder by running: +First, get the path of the Realm files from the simulator's Documents folder by running: ```sh open `xcrun simctl get_app_container booted com.your.app.bundleId data`/Documents diff --git a/contrib/scripts/check-exclude-icloud-backup.sh b/contrib/scripts/check-exclude-icloud-backup.sh index 41d4127697..306a1af412 100755 --- a/contrib/scripts/check-exclude-icloud-backup.sh +++ b/contrib/scripts/check-exclude-icloud-backup.sh @@ -1,12 +1,6 @@ #!/bin/bash -# Check if appbundle parameter is provided -if [ -z "$1" ]; then - echo "Usage: $0 " - exit 1 -fi - -appbundle=$1 +TEST_APP_BUNDLE_ID=com.microsoft.ReactTestApp # Check if a simulator is booted booted_simulators=$(xcrun simctl list | grep "Booted") @@ -25,15 +19,15 @@ if [ "$booted_count" -gt 1 ]; then fi # Extract the name of the booted simulator -booted_simulator=$(echo "$booted_simulator" | xargs) +booted_simulator=$(echo "$booted_simulators" | xargs) echo -e "Running script on simulator: $booted_simulator\n" # Get the app container path -app_container_path=$(xcrun simctl get_app_container booted "$appbundle" data 2>/dev/null) +app_container_path=$(xcrun simctl get_app_container booted "$TEST_APP_BUNDLE_ID" data 2>/dev/null) # Check if the command was successful if [ $? -ne 0 ] || [ -z "$app_container_path" ]; then - echo "Failed to get app container path for $appbundle" + echo "Failed to get app container path for $TEST_APP_BUNDLE_ID" exit 1 fi @@ -47,7 +41,7 @@ if [ ! -d "$documents_path" ]; then fi # Run xattr on all files in the directory -for file in "$documents_path"/*; do +for file in "$documents_path"/icloud-backup-tests/*.realm; do if [ -e "$file" ]; then filename=$(basename "$file") attrs=$(xattr "$file" 2>/dev/null) diff --git a/integration-tests/tests/src/tests.ts b/integration-tests/tests/src/tests.ts index 6bd005fd52..3be318c3d2 100644 --- a/integration-tests/tests/src/tests.ts +++ b/integration-tests/tests/src/tests.ts @@ -54,6 +54,7 @@ import "./tests/counter"; import "./tests/dictionary"; import "./tests/dynamic-schema-updates"; import "./tests/enums"; +import "./tests/exclude-from-icloud-backup"; import "./tests/iterators"; import "./tests/linking-objects"; import "./tests/list"; diff --git a/integration-tests/tests/src/tests/exclude-from-icloud-backup.ts b/integration-tests/tests/src/tests/exclude-from-icloud-backup.ts new file mode 100644 index 0000000000..1affd38980 --- /dev/null +++ b/integration-tests/tests/src/tests/exclude-from-icloud-backup.ts @@ -0,0 +1,53 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2024 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +import Realm from "realm"; + +const BASE_PATH = "icloud-backup-tests"; + +describe("icloud backup", () => { + // This is in a separate suite to avoid + // See contrib/guide-testing-exclude-icloud-backup.md to verify the result of the test + it("excludes", () => { + const realm = new Realm({ + path: `${BASE_PATH}/excluded.realm`, + schema: [], + excludeFromIcloudBackup: true, + }); + realm.close(); + }); + + it("removes exclusion", () => { + const path = `${BASE_PATH}/reincluded.realm`; + { + const realm = new Realm({ + path, + schema: [], + excludeFromIcloudBackup: true, + }); + realm.close(); + } + { + const realm = new Realm({ + path, + excludeFromIcloudBackup: false, + }); + realm.close(); + } + }); +}); diff --git a/packages/realm/bindgen/js_opt_in_spec.yml b/packages/realm/bindgen/js_opt_in_spec.yml index 8dc0439138..9a0db8184a 100644 --- a/packages/realm/bindgen/js_opt_in_spec.yml +++ b/packages/realm/bindgen/js_opt_in_spec.yml @@ -237,8 +237,8 @@ classes: - remove_realm_files_from_directory - remove_file - remove_directory + - exclude_from_icloud_backup - get_cpu_arch - - after_realm_open WeakSyncSession: methods: diff --git a/packages/realm/bindgen/js_spec.yml b/packages/realm/bindgen/js_spec.yml index c69f0cb3a0..5c1ca9d845 100644 --- a/packages/realm/bindgen/js_spec.yml +++ b/packages/realm/bindgen/js_spec.yml @@ -14,8 +14,8 @@ classes: remove_realm_files_from_directory: '(directory: const std::string&)' remove_file: '(path: const std::string&)' remove_directory: '(path: const std::string&)' + exclude_from_icloud_backup: '(path: const std::string&, value: bool)' get_cpu_arch: () -> std::string - after_realm_open: '(realm: SharedRealm, exclude_from_icloud_backup: bool)' # print: (const char* fmt, ...) # can't expose varargs directly. Could expose a fixed overload. WeakSyncSession: diff --git a/packages/realm/binding/android/src/main/cpp/platform.cpp b/packages/realm/binding/android/src/main/cpp/platform.cpp index 85300521c2..80b5e59693 100644 --- a/packages/realm/binding/android/src/main/cpp/platform.cpp +++ b/packages/realm/binding/android/src/main/cpp/platform.cpp @@ -123,6 +123,11 @@ void JsPlatformHelpers::remove_file(const std::string& path) fs::remove(path); } +void JsPlatformHelpers::exclude_from_icloud_backup(const std::string&, bool) +{ + // no-op +} + void JsPlatformHelpers::print(const char* fmt, ...) { va_list vl; @@ -131,8 +136,6 @@ void JsPlatformHelpers::print(const char* fmt, ...) va_end(vl); } -void JsPlatformHelpers::after_realm_open(SharedRealm, bool) {} - std::string JsPlatformHelpers::get_cpu_arch() { #if defined(__arm__) diff --git a/packages/realm/binding/apple/platform.mm b/packages/realm/binding/apple/platform.mm index 3408a6aba0..1de8271784 100644 --- a/packages/realm/binding/apple/platform.mm +++ b/packages/realm/binding/apple/platform.mm @@ -35,32 +35,6 @@ return error.localizedDescription; } -static void RLMCheckSkipBackupAttributeToItemAtPath(std::string_view path, bool exclude_from_icloud_backup) { - NSNumber *current; - - [[NSURL fileURLWithPath:@(path.data())] - getResourceValue:¤t - forKey:NSURLIsExcludedFromBackupKey - error:nil]; - - if (current.boolValue != exclude_from_icloud_backup) { - [[NSURL fileURLWithPath:@(path.data())] - setResourceValue:@(exclude_from_icloud_backup) - forKey:NSURLIsExcludedFromBackupKey - error:nil]; - - } -} - -static void RLMCheckSkipBackupAttributeToRealmFilesAtPath(std::string path, bool exclude_from_icloud_backup) { - const std::vector extensions = {"", ".lock", ".note", - ".management"}; - - for (const auto& ext : extensions) { - RLMCheckSkipBackupAttributeToItemAtPath(path + ext, exclude_from_icloud_backup); - } -} - static std::string s_default_realm_directory; namespace realm { @@ -184,15 +158,28 @@ static void RLMCheckSkipBackupAttributeToRealmFilesAtPath(std::string path, bool } } -void JsPlatformHelpers::after_realm_open(const SharedRealm realm, bool exclude_from_icloud_backup) { - RLMCheckSkipBackupAttributeToRealmFilesAtPath(realm->config().path, exclude_from_icloud_backup); -} - void JsPlatformHelpers::remove_directory(const std::string &path) { remove_file(path); // works for directories too } +void JsPlatformHelpers::exclude_from_icloud_backup(const std::string& path, bool value) { + NSNumber *current; + + [[NSURL fileURLWithPath:@(path.data())] + getResourceValue:¤t + forKey:NSURLIsExcludedFromBackupKey + error:nil]; + + if (current.boolValue != value) { + [[NSURL fileURLWithPath:@(path.data())] + setResourceValue:@(value) + forKey:NSURLIsExcludedFromBackupKey + error:nil]; + + } +} + void JsPlatformHelpers::print(const char *fmt, ...) { va_list vl; va_start(vl, fmt); diff --git a/packages/realm/binding/node/platform.cpp b/packages/realm/binding/node/platform.cpp index 17ed50d1ae..b7a7d3a8ae 100644 --- a/packages/realm/binding/node/platform.cpp +++ b/packages/realm/binding/node/platform.cpp @@ -209,6 +209,11 @@ void JsPlatformHelpers::remove_file(const std::string& path) } } +void JsPlatformHelpers::exclude_from_icloud_backup(const std::string&, bool) +{ + // no-op +} + void JsPlatformHelpers::print(const char* fmt, ...) { va_list vl; @@ -219,11 +224,6 @@ void JsPlatformHelpers::print(const char* fmt, ...) va_end(vl); } -void JsPlatformHelpers::after_realm_open(SharedRealm, bool) -{ - // no-op -} - // this should never be called std::string JsPlatformHelpers::get_cpu_arch() { diff --git a/packages/realm/binding/platform.hpp b/packages/realm/binding/platform.hpp index d9286e3998..a1eac15bc3 100644 --- a/packages/realm/binding/platform.hpp +++ b/packages/realm/binding/platform.hpp @@ -19,7 +19,6 @@ #pragma once #include -#include namespace realm { // @@ -56,7 +55,7 @@ class JsPlatformHelpers { // print something static void print(const char* fmt, ...); - // runs after the realm has been opened - static void after_realm_open(const SharedRealm realm, const bool exclude_from_icloud_backup = false); + // excludes the path from icloud backup on iOS and no-op on other platforms + static void exclude_from_icloud_backup(const std::string& path, bool value); }; } // namespace realm diff --git a/packages/realm/src/Realm.ts b/packages/realm/src/Realm.ts index 495f292e1f..37d1d1d222 100644 --- a/packages/realm/src/Realm.ts +++ b/packages/realm/src/Realm.ts @@ -579,7 +579,15 @@ export class Realm { this.schemaExtras = schemaExtras || {}; } - binding.JsPlatformHelpers.afterRealmOpen(this.internal, config.excludeFromIcloudBackup ?? false); + // Optionally: Exclude or include Realm files from iCloud backup + const { excludeFromIcloudBackup } = config; + if (typeof excludeFromIcloudBackup === "boolean") { + const realmPath = this.internal.config.path; + for (const fileNameSuffix of ["", ".lock", ".note", ".management"]) { + const filePath = realmPath + fileNameSuffix; + binding.JsPlatformHelpers.excludeFromIcloudBackup(filePath, excludeFromIcloudBackup); + } + } Object.defineProperty(this, "classes", { enumerable: false,