Skip to content

Commit

Permalink
Add option to exclude Realm file from iCloud backup (#6922)
Browse files Browse the repository at this point in the history
* Add option to exclude Realm file from iCloud backup

* Remove reference to deleted guide

* Update changelog

* Use better typedef

* Add flag as param to scope feature to realmJS only

* Update implementation to enable/disable icloud backup dynamically

* Add guide and script to check excludeFromBackup attribute

* Add missing requirement for android build

* Update wording in CHANGELOG.md and adding an example

---------

Co-authored-by: Kræn Hansen <[email protected]>
  • Loading branch information
danibonilha and kraenhansen authored Nov 8, 2024
1 parent 1ecd191 commit 93d7d3e
Show file tree
Hide file tree
Showing 12 changed files with 180 additions and 4 deletions.
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
* Fix build failure from duplicate libreactnative.so files. I.e. "2 files found with path 'lib/arm64-v8a/libreactnative.so' from inputs" ([#6918](https://github.com/realm/realm-js/issues/6918), since v12.13.2)
Expand Down
7 changes: 4 additions & 3 deletions contrib/building.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
50 changes: 50 additions & 0 deletions contrib/guide-testing-exclude-icloud-backup.md
Original file line number Diff line number Diff line change
@@ -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 <com.your.app.bundle>
```

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 <realm file simulator path>
```

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.
65 changes: 65 additions & 0 deletions contrib/scripts/check-exclude-icloud-backup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/bin/bash

# Check if appbundle parameter is provided
if [ -z "$1" ]; then
echo "Usage: $0 <com.app.bundle.id>"
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

1 change: 1 addition & 0 deletions packages/realm/bindgen/js_opt_in_spec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ classes:
- remove_file
- remove_directory
- get_cpu_arch
- after_realm_open

WeakSyncSession:
methods:
Expand Down
1 change: 1 addition & 0 deletions packages/realm/bindgen/js_spec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions packages/realm/binding/android/src/main/cpp/platform.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down
30 changes: 30 additions & 0 deletions packages/realm/binding/apple/platform.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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:&current
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<std::string> 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 {
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions packages/realm/binding/node/platform.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
4 changes: 4 additions & 0 deletions packages/realm/binding/platform.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#pragma once

#include <string>
#include <realm/object-store/shared_realm.hpp>

namespace realm {
//
Expand Down Expand Up @@ -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
6 changes: 6 additions & 0 deletions packages/realm/src/Configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 & {
Expand Down
2 changes: 2 additions & 0 deletions packages/realm/src/Realm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 93d7d3e

Please sign in to comment.