diff --git a/CHANGELOG.md b/CHANGELOG.md index 104eb6e2a3..132e3b3f2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ * None ### Enhancements -* None +* Added configuration option `App.baseFilePath` which controls where synced Realms and metadata is stored. ### Fixed * Fix type error when using `realm.create` in combination with class base models. (since v11.0.0) diff --git a/docs/realm.js b/docs/realm.js index 78ad153ed9..556926f0f7 100644 --- a/docs/realm.js +++ b/docs/realm.js @@ -395,7 +395,8 @@ class Realm { * Realm database should be stored. For synced Realms, a relative path is used together with app id and * user id in order to avoid collisions with other apps or users. An absolute path is left untouched * and on some platforms (iOS and Android) the app might not have permissions to create or open - * the file - permissions are not validated. + * the file - permissions are not validated. If a relative path is specified, it is relative to + * {@link Realm.App~AppConfiguration.baseFilePath}. * @property {string} [fifoFilesFallbackPath] - Opening a Realm creates a number of FIFO special files in order to * coordinate access to the Realm across threads and processes. If the Realm file is stored in a location * that does not allow the creation of FIFO special files (e.g. FAT32 filesystems), then the Realm cannot be opened. diff --git a/docs/sync.js b/docs/sync.js index 1cfbcd6254..cdef2bbf31 100644 --- a/docs/sync.js +++ b/docs/sync.js @@ -24,6 +24,7 @@ * @property {string} id - The id of the Atlas App Services application. * @property {string} [baseUrl] - The base URL of the Atlas App Services server. * @property {number} [timeout] - General timeout (in millisecs) for requests. + * @property {string} [baseFilePath] - Specify where synced Realms and metadata is stored. If not specified, the current work directory is used. * @property {Realm.App~LocalAppConfiguration} [app] - local app configuration */ diff --git a/integration-tests/tests/src/node/path.ts b/integration-tests/tests/src/node/path.ts index 95e6cecd55..f4560d3363 100644 --- a/integration-tests/tests/src/node/path.ts +++ b/integration-tests/tests/src/node/path.ts @@ -20,14 +20,16 @@ import { expect } from "chai"; import Realm, { BSON } from "realm"; import path from "node:path"; import os from "node:os"; +import { existsSync, rmSync } from "node:fs"; import { importAppBefore, authenticateUserBefore } from "../hooks"; +import { importApp } from "../utils/import-app"; const getAbsolutePath = () => os.tmpdir() + path.sep + new BSON.UUID().toHexString(); const getRelativePath = () => "testFiles" + path.sep + new BSON.UUID().toHexString(); const getPartitionValue = () => new BSON.UUID().toHexString(); -const schema = { +const Schema = { name: "MixedClass", primaryKey: "_id", properties: { @@ -36,10 +38,12 @@ const schema = { }, }; +const FlexibleSchema = { ...Schema, properties: { ...Schema.properties, nonQueryable: "string?" } }; + describe("path configuration (local)", function () { it("relative path", function () { const filename = getRelativePath(); - const realm = new Realm({ path: filename, schema: [schema] }); + const realm = new Realm({ path: filename, schema: [Schema] }); expect(realm.path.endsWith(filename)).to.be.true; realm.close(); Realm.deleteFile({ path: filename }); @@ -47,13 +51,39 @@ describe("path configuration (local)", function () { it("absolute path", function () { const filename = getAbsolutePath(); - const realm = new Realm({ path: filename, schema: [schema] }); + const realm = new Realm({ path: filename, schema: [Schema] }); expect(realm.path).to.equal(filename); realm.close(); Realm.deleteFile({ path: filename }); }); }); +describe.skipIf(environment.missingServer, `app configuration of root directory (flexible sync)`, async function () { + const { appId, baseUrl } = await importApp("with-db-flx"); + + it("directory and file created where expected", async function () { + const tmpdir = getAbsolutePath(); + expect(fs.exists(tmpdir)).to.be.false; + + const app = new Realm.App({ id: appId, baseUrl, baseFilePath: tmpdir }); + const user = await app.logIn(Realm.Credentials.anonymous()); + + const realm = await Realm.open({ + schema: [FlexibleSchema], + sync: { + flexible: true, + user, + }, + }); + + expect(existsSync(tmpdir)).to.be.true; + expect(realm.path.startsWith(tmpdir)); + + realm.close(); + rmSync(tmpdir, { recursive: true }); + }); +}); + describe.skipIf(environment.missingServer, "path configuration (partition based sync)", function () { importAppBefore("with-db"); authenticateUserBefore(); @@ -62,7 +92,7 @@ describe.skipIf(environment.missingServer, "path configuration (partition based const filename = getAbsolutePath(); const realm = await Realm.open({ path: filename, - schema: [schema], + schema: [Schema], sync: { partitionValue: getPartitionValue(), user: this.user, @@ -77,7 +107,7 @@ describe.skipIf(environment.missingServer, "path configuration (partition based const filename = getRelativePath(); const realm = await Realm.open({ path: filename, - schema: [schema], + schema: [Schema], sync: { partitionValue: getPartitionValue(), user: this.user, @@ -98,7 +128,7 @@ describe.skipIf(environment.skipFlexibleSync, "path configuration (flexible sync const filename = getAbsolutePath(); const realm = await Realm.open({ path: filename, - schema: [schema], + schema: [FlexibleSchema], sync: { flexible: true, user: this.user, @@ -114,7 +144,7 @@ describe.skipIf(environment.skipFlexibleSync, "path configuration (flexible sync const filename = getRelativePath(); const realm = await Realm.open({ path: filename, - schema: [schema], + schema: [FlexibleSchema], sync: { flexible: true, user: this.user, diff --git a/src/js_app.hpp b/src/js_app.hpp index 5ef5661286..f1aed13e4b 100644 --- a/src/js_app.hpp +++ b/src/js_app.hpp @@ -34,6 +34,7 @@ #include "js_network_transport.hpp" #include "js_email_password_auth.hpp" #include "realm/object-store/sync/subscribable.hpp" +#include "realm/util/file.hpp" using SharedApp = std::shared_ptr; @@ -170,6 +171,7 @@ void AppClass::constructor(ContextType ctx, ObjectType this_object, Arguments static const String config_app = "app"; static const String config_app_name = "name"; static const String config_app_version = "version"; + static const String config_base_file_path = "baseFilePath"; args.validate_count(1); @@ -178,6 +180,11 @@ void AppClass::constructor(ContextType ctx, ObjectType this_object, Arguments std::string id; realm::app::App::Config config; + SyncClientConfig client_config; + client_config.metadata_mode = SyncManager::MetadataMode::NoEncryption; + client_config.user_agent_binding_info = get_user_agent(); + client_config.base_file_path = default_realm_file_directory(); // this may be changed + if (Value::is_object(ctx, args[0])) { ObjectType config_object = Value::validated_to_object(ctx, args[0]); @@ -217,6 +224,11 @@ void AppClass::constructor(ContextType ctx, ObjectType this_object, Arguments std::optional(Value::validated_to_string(ctx, config_app_version_value, "version")); } } + + ValueType base_file_path_value = Object::get_property(ctx, config_object, config_base_file_path); + if (!Value::is_undefined(ctx, base_file_path_value)) { + client_config.base_file_path = Value::validated_to_string(ctx, base_file_path_value); + } } else if (Value::is_string(ctx, args[0])) { config.app_id = Value::validated_to_string(ctx, args[0]); @@ -238,13 +250,8 @@ void AppClass::constructor(ContextType ctx, ObjectType this_object, Arguments config.device_info.framework_name = framework_name; config.device_info.framework_version = framework_version; - auto realm_file_directory = default_realm_file_directory(); - ensure_directory_exists_for_file(realm_file_directory); - - SyncClientConfig client_config; - client_config.base_file_path = realm_file_directory; - client_config.metadata_mode = SyncManager::MetadataMode::NoEncryption; - client_config.user_agent_binding_info = get_user_agent(); + util::try_make_dir(client_config.base_file_path); + set_default_realm_file_directory(client_config.base_file_path); SharedApp app = app::App::get_shared_app(config, client_config); diff --git a/src/node/platform.cpp b/src/node/platform.cpp index c05e6e04f6..0996c82ec6 100644 --- a/src/node/platform.cpp +++ b/src/node/platform.cpp @@ -24,6 +24,8 @@ #include "../platform.hpp" +static std::string s_default_realm_directory; + namespace realm { class UVException : public std::runtime_error { @@ -44,9 +46,19 @@ struct FileSystemRequest : uv_fs_t { } }; +void set_default_realm_file_directory(std::string dir) +{ + s_default_realm_directory = dir; +} + // taken from Node.js: function Cwd in node.cc std::string default_realm_file_directory() { + + if (!s_default_realm_directory.empty()) { + return s_default_realm_directory; + } + #ifdef _WIN32 /* MAX_PATH is in characters, not bytes. Make sure we have enough headroom. */ char buf[MAX_PATH * 4]; diff --git a/types/app.d.ts b/types/app.d.ts index fe548ae820..a37c71f29f 100644 --- a/types/app.d.ts +++ b/types/app.d.ts @@ -355,6 +355,11 @@ declare namespace Realm { */ baseUrl?: string; + /** + * An optional path to a directory where synced Realms are stored. + */ + baseFilePath?: string; + /** * This describes the local app, sent to the server when a user authenticates. * Specifying this will enable the server to respond differently to specific versions of specific apps.