diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ce1cba826..49d55d2be3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,16 @@ * None ### Enhancements -* None +* Added `excludeFromIcloudBackup` option to the `Realm` constructor to exclude the realm files from iCloud backup. ([#4139](https://github.com/realm/realm-js/issues/4139)) +```typescript +const realm = new Realm({ + schema: [ + /* your schema */ + ], + // Set to true to exclude from iCloud backup, false to include, defaults to false + excludeFromIcloudBackup: true, +}); +``` ### Fixed * ([#????](https://github.com/realm/realm-js/issues/????), since v?.?.?) diff --git a/contrib/building.md b/contrib/building.md index 96b28615a9..7d5c72a7b3 100644 --- a/contrib/building.md +++ b/contrib/building.md @@ -52,6 +52,7 @@ The following dependencies are required. All except Xcode can be installed by fo - [Android NDK 23](https://developer.android.com/ndk/downloads/index.html) - [Android CMake](https://developer.android.com/ndk/guides/cmake) - [Docker](https://www.docker.com/) is used for testing. You can install it [as a desktop app](https://www.docker.com/products/docker-desktop/) or through Homebrew: `brew install --cask docker`. +- [Ninja](https://ninja-build.org/) `brew install ninja` Moreover, in order to avoid introducing false positives in our analytics dataset, it is highly recommended to disable analytics by adding the following to your shell configuration: @@ -83,9 +84,10 @@ brew install cmake #### Android -First, install OpenJDK: +First, install OpenJDK and Ninja: ```sh +brew install ninja brew install --cask temurin # Check this returns: openjdk version "18.0.1" 2022-04-19 @@ -384,9 +386,8 @@ export NVM_DIR="$HOME/.nvm" ## Testing against real apps -There are a couple of suggested workflows for testing your changes to Realm JS against real apps: +Here's the suggested workflow for testing your changes to Realm JS against real apps: -- [Guide: Setting up watchman to copy changes from this package to an app](guide-watchman.md) - [Guide: Testing your changes against sample apps using a script](guide-testing-with-sample-apps.md) ## Debugging diff --git a/contrib/guide-testing-exclude-icloud-backup.md b/contrib/guide-testing-exclude-icloud-backup.md new file mode 100644 index 0000000000..fe57cddff1 --- /dev/null +++ b/contrib/guide-testing-exclude-icloud-backup.md @@ -0,0 +1,50 @@ +# Guide: Testing Exclude iCloud Backup + +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 +const realmConfig = { + schema: [ + /* your schema */ + ], + path: "default.realm", + excludeFromIcloudBackup: true, // Set to true to exclude from iCloud backup, false to include, defaults to false +}; + +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: + +```sh +open `xcrun simctl get_app_container booted com.your.app.bundleId data`/Documents +``` + +This will open a Finder window with the files. Drag and drop each file to the terminal after adding `xattr`: + +```sh +xattr +``` + +If this command returns: + +```sh +com.apple.metadata:com_apple_backup_excludeItem +``` + +It means the file has been successfully marked as excluded from backup 🎉. If it returns nothing, the file has no attributes and is not excluded from backup. diff --git a/contrib/scripts/check-exclude-icloud-backup.sh b/contrib/scripts/check-exclude-icloud-backup.sh new file mode 100755 index 0000000000..41d4127697 --- /dev/null +++ b/contrib/scripts/check-exclude-icloud-backup.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +# Check if appbundle parameter is provided +if [ -z "$1" ]; then + echo "Usage: $0 " + exit 1 +fi + +appbundle=$1 + +# Check if a simulator is booted +booted_simulators=$(xcrun simctl list | grep "Booted") + +if [ -z "$booted_simulators" ]; then + echo "No simulator booted. Please boot a simulator with the React Native app running and re-run the script." + exit 1 +fi + +# Count the number of booted simulators +booted_count=$(echo "$booted_simulators" | wc -l) + +if [ "$booted_count" -gt 1 ]; then + echo "More than one simulator is booted. Please keep only one open and re-run the script." + exit 1 +fi + +# Extract the name of the booted simulator +booted_simulator=$(echo "$booted_simulator" | 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) + +# Check if the command was successful +if [ $? -ne 0 ] || [ -z "$app_container_path" ]; then + echo "Failed to get app container path for $appbundle" + exit 1 +fi + +# Append /Documents to the path +documents_path="$app_container_path/Documents" + +# Check if the directory exists +if [ ! -d "$documents_path" ]; then + echo "Documents directory does not exist at $documents_path" + exit 1 +fi + +# Run xattr on all files in the directory +for file in "$documents_path"/*; do + if [ -e "$file" ]; then + filename=$(basename "$file") + attrs=$(xattr "$file" 2>/dev/null) + if [ -z "$attrs" ]; then + echo -e "\033[1;33m$filename:\033[0m no attributes set ❌" + else + if echo "$attrs" | grep -q "com_apple_backup_excludeItem"; then + echo -e "\033[0;32m\033[1m$filename:\033[0m: $attrs\033[0;32m ✅\033[0m" + else + echo "$filename: $attrs" + fi + fi + fi +done + diff --git a/packages/realm/bindgen/js_opt_in_spec.yml b/packages/realm/bindgen/js_opt_in_spec.yml index ef80c1274e..8dc0439138 100644 --- a/packages/realm/bindgen/js_opt_in_spec.yml +++ b/packages/realm/bindgen/js_opt_in_spec.yml @@ -238,6 +238,7 @@ classes: - remove_file - remove_directory - 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 b6e9f28542..c69f0cb3a0 100644 --- a/packages/realm/bindgen/js_spec.yml +++ b/packages/realm/bindgen/js_spec.yml @@ -15,6 +15,7 @@ classes: remove_file: '(path: const std::string&)' remove_directory: '(path: const std::string&)' 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 01072cf74a..29d92d162d 100644 --- a/packages/realm/binding/android/src/main/cpp/platform.cpp +++ b/packages/realm/binding/android/src/main/cpp/platform.cpp @@ -115,6 +115,8 @@ 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 9df0ba724e..3408a6aba0 100644 --- a/packages/realm/binding/apple/platform.mm +++ b/packages/realm/binding/apple/platform.mm @@ -35,6 +35,32 @@ 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 { @@ -158,6 +184,10 @@ } } +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 diff --git a/packages/realm/binding/node/platform.cpp b/packages/realm/binding/node/platform.cpp index da75d9217f..17ed50d1ae 100644 --- a/packages/realm/binding/node/platform.cpp +++ b/packages/realm/binding/node/platform.cpp @@ -219,6 +219,11 @@ 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 66d5d3e451..d9286e3998 100644 --- a/packages/realm/binding/platform.hpp +++ b/packages/realm/binding/platform.hpp @@ -19,6 +19,7 @@ #pragma once #include +#include namespace realm { // @@ -54,5 +55,8 @@ 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); }; } // namespace realm diff --git a/packages/realm/src/Configuration.ts b/packages/realm/src/Configuration.ts index 76638578f0..b06fc17386 100644 --- a/packages/realm/src/Configuration.ts +++ b/packages/realm/src/Configuration.ts @@ -160,6 +160,12 @@ export type BaseConfiguration = { */ onFirstOpen?: (realm: Realm) => void; migrationOptions?: MigrationOptions; + /** + * Specifies if this Realm should be excluded from iCloud backup. + * @default false + * @since 12.13.3 + */ + excludeFromIcloudBackup?: boolean; }; export type ConfigurationWithSync = BaseConfiguration & { diff --git a/packages/realm/src/Realm.ts b/packages/realm/src/Realm.ts index 748556265c..495f292e1f 100644 --- a/packages/realm/src/Realm.ts +++ b/packages/realm/src/Realm.ts @@ -579,6 +579,8 @@ export class Realm { this.schemaExtras = schemaExtras || {}; } + binding.JsPlatformHelpers.afterRealmOpen(this.internal, config.excludeFromIcloudBackup ?? false); + Object.defineProperty(this, "classes", { enumerable: false, configurable: false,