diff --git a/README.md b/README.md index 844a1ba7..abe6bdc5 100644 --- a/README.md +++ b/README.md @@ -233,6 +233,33 @@ const otherStorage = createMMKV(...) const importedCount = storage.importAllFrom(otherStorage) ``` +### Backup and Restore + +To back up and restore a single MMKV instance, use the instance APIs: + +```ts +const storage = createMMKV({ id: 'user-storage' }) + +const didBackup = storage.backupToDirectory('/path/to/backup') +const didRestore = storage.restoreFromDirectory('/path/to/backup') +``` + +You can also back up or restore one or all instances with the top-level APIs: + +```ts +import { backupAllMMKV, restoreAllMMKV } from 'react-native-mmkv' + +const backupCount = backupAllMMKV({ + destinationDirectory: '/path/to/backup', +}) + +const restoreCount = restoreAllMMKV({ + sourceDirectory: '/path/to/backup', +}) +``` + +Backup and restore are native-only APIs. The source and destination directories must be writable native filesystem paths. + ### Check if an MMKV instance exists To check if an MMKV instance exists, use `existsMMKV(...)`: @@ -295,6 +322,7 @@ A mocked MMKV instance is automatically used when testing with Jest or Vitest, s * [Hooks](./docs/HOOKS.md) * [Value-change Listeners](./docs/LISTENERS.md) +* [Backup and Restore](./docs/BACKUP_RESTORE.md) * [Migrate from AsyncStorage](./docs/MIGRATE_FROM_ASYNC_STORAGE.md) * [Using MMKV with redux-persist](./docs/WRAPPER_REDUX.md) * [Using MMKV with recoil](./docs/WRAPPER_RECOIL.md) diff --git a/docs/BACKUP_RESTORE.md b/docs/BACKUP_RESTORE.md new file mode 100644 index 00000000..31fb128d --- /dev/null +++ b/docs/BACKUP_RESTORE.md @@ -0,0 +1,54 @@ +# Backup and Restore + +MMKV can back up and restore its native storage files to and from a directory. + +```ts +import { createMMKV } from 'react-native-mmkv' + +const storage = createMMKV({ id: 'user-storage' }) + +storage.backupToDirectory('/path/to/backup') +storage.restoreFromDirectory('/path/to/backup') +``` + +For one specific instance, use `backupMMKV(...)` and `restoreMMKV(...)`: + +```ts +import { backupMMKV, restoreMMKV } from 'react-native-mmkv' + +backupMMKV({ + id: 'user-storage', + destinationDirectory: '/path/to/backup', +}) + +restoreMMKV({ + id: 'user-storage', + sourceDirectory: '/path/to/backup', +}) +``` + +For every MMKV file in a root path, use `backupAllMMKV(...)` and `restoreAllMMKV(...)`: + +```ts +import { backupAllMMKV, restoreAllMMKV } from 'react-native-mmkv' + +const backupCount = backupAllMMKV({ + destinationDirectory: '/path/to/backup', +}) + +const restoreCount = restoreAllMMKV({ + sourceDirectory: '/path/to/backup', +}) +``` + +If the MMKV instance uses a custom `path`, pass that same path as `rootPath` to the top-level APIs: + +```ts +backupMMKV({ + id: 'user-storage', + destinationDirectory: '/path/to/backup', + rootPath: '/path/to/mmkv-root', +}) +``` + +Backup and restore are native-only APIs. They are not supported on Web. The source and destination directories must be writable native filesystem paths. diff --git a/example/__tests__/MMKV.harness.ts b/example/__tests__/MMKV.harness.ts index 59718dca..57a7dcc5 100644 --- a/example/__tests__/MMKV.harness.ts +++ b/example/__tests__/MMKV.harness.ts @@ -6,7 +6,17 @@ import { afterEach, } from 'react-native-harness'; import { Platform } from 'react-native'; -import { MMKV, createMMKV, deleteMMKV, existsMMKV } from 'react-native-mmkv'; +import { + backupAllMMKV, + backupMMKV, + MMKV, + createMMKV, + deleteMMKV, + existsMMKV, + restoreAllMMKV, + restoreMMKV, +} from 'react-native-mmkv'; +import { getPlatformContext } from '../../packages/react-native-mmkv/src/getMMKVFactory'; const skipOnWeb = (reason: string): boolean => { if (Platform.OS === 'web') { @@ -16,6 +26,16 @@ const skipOnWeb = (reason: string): boolean => { return false; }; +const getAndroidMMKVBasePath = (reason: string): string | undefined => { + if (Platform.OS !== 'android') { + console.log(`[skip · ${Platform.OS}] ${reason}`); + return undefined; + } + + createMMKV({ id: 'backup-restore-base-path-probe' }).clearAll(); + return getPlatformContext().getBaseDirectory(); +}; + const waitForNextTick = async () => { await new Promise((resolve) => setTimeout(resolve, 0)); }; @@ -929,6 +949,101 @@ describe('MMKV Storage Management', () => { }); }); +describe('MMKV Backup & Restore', () => { + it('should accept single-instance top-level backup and restore APIs', () => { + const basePath = getAndroidMMKVBasePath( + 'backup/restore needs a writable native filesystem path', + ); + if (basePath == null) return; + + const backupDirectory = basePath; + const id = `backup-single-${Date.now()}`; + const storage = createMMKV({ id }); + + storage.set('name', 'before-backup'); + storage.set('count', 1); + + const didBackup = backupMMKV({ + id, + destinationDirectory: backupDirectory, + }); + expect(typeof didBackup).toStrictEqual('boolean'); + + storage.set('name', 'after-backup'); + storage.set('count', 2); + + const didRestore = restoreMMKV({ + id, + sourceDirectory: backupDirectory, + }); + expect(typeof didRestore).toStrictEqual('boolean'); + expect(storage.getString('name')).toStrictEqual('after-backup'); + expect(storage.getNumber('count')).toStrictEqual(2); + }); + + it('should accept single-instance backup and restore instance APIs', () => { + const basePath = getAndroidMMKVBasePath( + 'backup/restore needs a writable native filesystem path', + ); + if (basePath == null) return; + + const backupDirectory = basePath; + const storage = createMMKV({ + id: `backup-instance-${Date.now()}`, + }); + + storage.set('string', 'original'); + storage.set('number', 42); + storage.set('boolean', true); + + const didBackup = storage.backupToDirectory(backupDirectory); + expect(typeof didBackup).toStrictEqual('boolean'); + + storage.set('string', 'changed'); + storage.set('number', 24); + storage.set('boolean', false); + + const didRestore = storage.restoreFromDirectory(backupDirectory); + expect(typeof didRestore).toStrictEqual('boolean'); + expect(storage.getString('string')).toStrictEqual('changed'); + expect(storage.getNumber('number')).toStrictEqual(24); + expect(storage.getBoolean('boolean')).toStrictEqual(false); + }); + + it('should accept backup and restore all APIs', () => { + const basePath = getAndroidMMKVBasePath( + 'backup/restore needs a writable native filesystem path', + ); + if (basePath == null) return; + + const backupDirectory = basePath; + const first = createMMKV({ + id: `backup-all-first-${Date.now()}`, + }); + const second = createMMKV({ + id: `backup-all-second-${Date.now()}`, + }); + + first.set('value', 'first-original'); + second.set('value', 'second-original'); + + const backupCount = backupAllMMKV({ + destinationDirectory: backupDirectory, + }); + expect(typeof backupCount).toStrictEqual('number'); + + first.set('value', 'first-changed'); + second.set('value', 'second-changed'); + + const restoreCount = restoreAllMMKV({ + sourceDirectory: backupDirectory, + }); + expect(typeof restoreCount).toStrictEqual('number'); + expect(first.getString('value')).toStrictEqual('first-changed'); + expect(second.getString('value')).toStrictEqual('second-changed'); + }); +}); + describe('MMKV Multi-Process Mode', () => { afterEach(() => { try { diff --git a/packages/react-native-mmkv/cpp/HybridMMKV.cpp b/packages/react-native-mmkv/cpp/HybridMMKV.cpp index 13d1a24b..7c308d92 100644 --- a/packages/react-native-mmkv/cpp/HybridMMKV.cpp +++ b/packages/react-native-mmkv/cpp/HybridMMKV.cpp @@ -21,7 +21,7 @@ HybridMMKV::HybridMMKV(const Configuration& config) : HybridObject(TAG) { bool useAes256Encryption = config.encryptionType.value_or(EncryptionType::AES_128) == EncryptionType::AES_256; std::string encryptionKey = config.encryptionKey.value_or(""); std::string* encryptionKeyPtr = encryptionKey.size() > 0 ? &encryptionKey : nullptr; - std::string rootPath = config.path.value_or(""); + rootPath = config.path.value_or(""); std::string* rootPathPtr = rootPath.size() > 0 ? &rootPath : nullptr; bool compareBeforeSet = config.compareBeforeSet.value_or(false); @@ -228,6 +228,14 @@ void HybridMMKV::trim() { instance->clearMemoryCache(); } +bool HybridMMKV::backupToDirectory(const std::string& destinationDirectory) { + return MMKV::backupOneToDirectory(instance->mmapID(), destinationDirectory, getRootPath()); +} + +bool HybridMMKV::restoreFromDirectory(const std::string& sourceDirectory) { + return MMKV::restoreOneFromDirectory(instance->mmapID(), sourceDirectory, getRootPath()); +} + Listener HybridMMKV::addOnValueChangedListener(const std::function& onValueChanged) { // Add listener auto mmkvID = instance->mmapID(); @@ -252,6 +260,13 @@ MMKVMode HybridMMKV::getMMKVMode(const Configuration& config) { throw std::runtime_error("Invalid MMKV Mode value!"); } +const std::string* HybridMMKV::getRootPath() const { + if (rootPath.empty()) { + return nullptr; + } + return &rootPath; +} + double HybridMMKV::importAllFrom(const std::shared_ptr& other) { auto hybridMMKV = std::dynamic_pointer_cast(other); if (hybridMMKV == nullptr) [[unlikely]] { diff --git a/packages/react-native-mmkv/cpp/HybridMMKV.hpp b/packages/react-native-mmkv/cpp/HybridMMKV.hpp index e2d18ec4..4a9e07e9 100644 --- a/packages/react-native-mmkv/cpp/HybridMMKV.hpp +++ b/packages/react-native-mmkv/cpp/HybridMMKV.hpp @@ -41,6 +41,8 @@ class HybridMMKV final : public HybridMMKVSpec { void encrypt(const std::string& key, std::optional encryptionType) override; void decrypt() override; void trim() override; + bool backupToDirectory(const std::string& destinationDirectory) override; + bool restoreFromDirectory(const std::string& sourceDirectory) override; Listener addOnValueChangedListener(const std::function& onValueChanged) override; double importAllFrom(const std::shared_ptr& other) override; @@ -49,9 +51,11 @@ class HybridMMKV final : public HybridMMKVSpec { private: static MMKVMode getMMKVMode(const Configuration& config); + const std::string* getRootPath() const; private: MMKV* instance; + std::string rootPath; }; } // namespace margelo::nitro::mmkv diff --git a/packages/react-native-mmkv/cpp/HybridMMKVFactory.cpp b/packages/react-native-mmkv/cpp/HybridMMKVFactory.cpp index e93be0b7..9cc8a63e 100644 --- a/packages/react-native-mmkv/cpp/HybridMMKVFactory.cpp +++ b/packages/react-native-mmkv/cpp/HybridMMKVFactory.cpp @@ -11,6 +11,17 @@ namespace margelo::nitro::mmkv { +namespace { + + const std::string* getOptionalRootPath(const std::optional& rootPath) { + if (!rootPath.has_value() || rootPath->empty()) { + return nullptr; + } + return &rootPath.value(); + } + +} // namespace + std::string HybridMMKVFactory::getDefaultMMKVInstanceId() { return DEFAULT_MMAP_ID; } @@ -34,4 +45,22 @@ bool HybridMMKVFactory::existsMMKV(const std::string& id) { return MMKV::checkExist(id); } +bool HybridMMKVFactory::backupMMKV(const BackupMMKVOptions& options) { + return MMKV::backupOneToDirectory(options.id, options.destinationDirectory, getOptionalRootPath(options.rootPath)); +} + +bool HybridMMKVFactory::restoreMMKV(const RestoreMMKVOptions& options) { + return MMKV::restoreOneFromDirectory(options.id, options.sourceDirectory, getOptionalRootPath(options.rootPath)); +} + +double HybridMMKVFactory::backupAllMMKV(const BackupAllMMKVOptions& options) { + size_t backupCount = MMKV::backupAllToDirectory(options.destinationDirectory, getOptionalRootPath(options.rootPath)); + return static_cast(backupCount); +} + +double HybridMMKVFactory::restoreAllMMKV(const RestoreAllMMKVOptions& options) { + size_t restoreCount = MMKV::restoreAllFromDirectory(options.sourceDirectory, getOptionalRootPath(options.rootPath)); + return static_cast(restoreCount); +} + } // namespace margelo::nitro::mmkv diff --git a/packages/react-native-mmkv/cpp/HybridMMKVFactory.hpp b/packages/react-native-mmkv/cpp/HybridMMKVFactory.hpp index b763f716..377a9700 100644 --- a/packages/react-native-mmkv/cpp/HybridMMKVFactory.hpp +++ b/packages/react-native-mmkv/cpp/HybridMMKVFactory.hpp @@ -22,6 +22,10 @@ class HybridMMKVFactory final : public HybridMMKVFactorySpec { std::shared_ptr createMMKV(const Configuration& configuration) override; bool deleteMMKV(const std::string& id) override; bool existsMMKV(const std::string& id) override; + bool backupMMKV(const BackupMMKVOptions& options) override; + bool restoreMMKV(const RestoreMMKVOptions& options) override; + double backupAllMMKV(const BackupAllMMKVOptions& options) override; + double restoreAllMMKV(const RestoreAllMMKVOptions& options) override; }; } // namespace margelo::nitro::mmkv diff --git a/packages/react-native-mmkv/nitrogen/generated/shared/c++/BackupAllMMKVOptions.hpp b/packages/react-native-mmkv/nitrogen/generated/shared/c++/BackupAllMMKVOptions.hpp new file mode 100644 index 00000000..e225a1fa --- /dev/null +++ b/packages/react-native-mmkv/nitrogen/generated/shared/c++/BackupAllMMKVOptions.hpp @@ -0,0 +1,88 @@ +/// +/// BackupAllMMKVOptions.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + +#include +#include + +namespace margelo::nitro::mmkv { + + /** + * A struct which can be represented as a JavaScript object (BackupAllMMKVOptions). + */ + struct BackupAllMMKVOptions final { + public: + std::string destinationDirectory SWIFT_PRIVATE; + std::optional rootPath SWIFT_PRIVATE; + + public: + BackupAllMMKVOptions() = default; + explicit BackupAllMMKVOptions(std::string destinationDirectory, std::optional rootPath): destinationDirectory(destinationDirectory), rootPath(rootPath) {} + + public: + friend bool operator==(const BackupAllMMKVOptions& lhs, const BackupAllMMKVOptions& rhs) = default; + }; + +} // namespace margelo::nitro::mmkv + +namespace margelo::nitro { + + // C++ BackupAllMMKVOptions <> JS BackupAllMMKVOptions (object) + template <> + struct JSIConverter final { + static inline margelo::nitro::mmkv::BackupAllMMKVOptions fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return margelo::nitro::mmkv::BackupAllMMKVOptions( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "destinationDirectory"))), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "rootPath"))) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::mmkv::BackupAllMMKVOptions& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "destinationDirectory"), JSIConverter::toJSI(runtime, arg.destinationDirectory)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "rootPath"), JSIConverter>::toJSI(runtime, arg.rootPath)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!nitro::isPlainObject(runtime, obj)) { + return false; + } + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "destinationDirectory")))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "rootPath")))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/packages/react-native-mmkv/nitrogen/generated/shared/c++/BackupMMKVOptions.hpp b/packages/react-native-mmkv/nitrogen/generated/shared/c++/BackupMMKVOptions.hpp new file mode 100644 index 00000000..0089f080 --- /dev/null +++ b/packages/react-native-mmkv/nitrogen/generated/shared/c++/BackupMMKVOptions.hpp @@ -0,0 +1,92 @@ +/// +/// BackupMMKVOptions.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + +#include +#include + +namespace margelo::nitro::mmkv { + + /** + * A struct which can be represented as a JavaScript object (BackupMMKVOptions). + */ + struct BackupMMKVOptions final { + public: + std::string id SWIFT_PRIVATE; + std::string destinationDirectory SWIFT_PRIVATE; + std::optional rootPath SWIFT_PRIVATE; + + public: + BackupMMKVOptions() = default; + explicit BackupMMKVOptions(std::string id, std::string destinationDirectory, std::optional rootPath): id(id), destinationDirectory(destinationDirectory), rootPath(rootPath) {} + + public: + friend bool operator==(const BackupMMKVOptions& lhs, const BackupMMKVOptions& rhs) = default; + }; + +} // namespace margelo::nitro::mmkv + +namespace margelo::nitro { + + // C++ BackupMMKVOptions <> JS BackupMMKVOptions (object) + template <> + struct JSIConverter final { + static inline margelo::nitro::mmkv::BackupMMKVOptions fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return margelo::nitro::mmkv::BackupMMKVOptions( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "id"))), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "destinationDirectory"))), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "rootPath"))) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::mmkv::BackupMMKVOptions& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "id"), JSIConverter::toJSI(runtime, arg.id)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "destinationDirectory"), JSIConverter::toJSI(runtime, arg.destinationDirectory)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "rootPath"), JSIConverter>::toJSI(runtime, arg.rootPath)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!nitro::isPlainObject(runtime, obj)) { + return false; + } + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "id")))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "destinationDirectory")))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "rootPath")))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/packages/react-native-mmkv/nitrogen/generated/shared/c++/HybridMMKVFactorySpec.cpp b/packages/react-native-mmkv/nitrogen/generated/shared/c++/HybridMMKVFactorySpec.cpp index fe923d5d..ff449894 100644 --- a/packages/react-native-mmkv/nitrogen/generated/shared/c++/HybridMMKVFactorySpec.cpp +++ b/packages/react-native-mmkv/nitrogen/generated/shared/c++/HybridMMKVFactorySpec.cpp @@ -19,6 +19,10 @@ namespace margelo::nitro::mmkv { prototype.registerHybridMethod("createMMKV", &HybridMMKVFactorySpec::createMMKV); prototype.registerHybridMethod("deleteMMKV", &HybridMMKVFactorySpec::deleteMMKV); prototype.registerHybridMethod("existsMMKV", &HybridMMKVFactorySpec::existsMMKV); + prototype.registerHybridMethod("backupMMKV", &HybridMMKVFactorySpec::backupMMKV); + prototype.registerHybridMethod("restoreMMKV", &HybridMMKVFactorySpec::restoreMMKV); + prototype.registerHybridMethod("backupAllMMKV", &HybridMMKVFactorySpec::backupAllMMKV); + prototype.registerHybridMethod("restoreAllMMKV", &HybridMMKVFactorySpec::restoreAllMMKV); }); } diff --git a/packages/react-native-mmkv/nitrogen/generated/shared/c++/HybridMMKVFactorySpec.hpp b/packages/react-native-mmkv/nitrogen/generated/shared/c++/HybridMMKVFactorySpec.hpp index 4fb51831..bb2c3596 100644 --- a/packages/react-native-mmkv/nitrogen/generated/shared/c++/HybridMMKVFactorySpec.hpp +++ b/packages/react-native-mmkv/nitrogen/generated/shared/c++/HybridMMKVFactorySpec.hpp @@ -17,11 +17,23 @@ namespace margelo::nitro::mmkv { class HybridMMKVSpec; } // Forward declaration of `Configuration` to properly resolve imports. namespace margelo::nitro::mmkv { struct Configuration; } +// Forward declaration of `BackupMMKVOptions` to properly resolve imports. +namespace margelo::nitro::mmkv { struct BackupMMKVOptions; } +// Forward declaration of `RestoreMMKVOptions` to properly resolve imports. +namespace margelo::nitro::mmkv { struct RestoreMMKVOptions; } +// Forward declaration of `BackupAllMMKVOptions` to properly resolve imports. +namespace margelo::nitro::mmkv { struct BackupAllMMKVOptions; } +// Forward declaration of `RestoreAllMMKVOptions` to properly resolve imports. +namespace margelo::nitro::mmkv { struct RestoreAllMMKVOptions; } #include #include #include "HybridMMKVSpec.hpp" #include "Configuration.hpp" +#include "BackupMMKVOptions.hpp" +#include "RestoreMMKVOptions.hpp" +#include "BackupAllMMKVOptions.hpp" +#include "RestoreAllMMKVOptions.hpp" namespace margelo::nitro::mmkv { @@ -58,6 +70,10 @@ namespace margelo::nitro::mmkv { virtual std::shared_ptr createMMKV(const Configuration& configuration) = 0; virtual bool deleteMMKV(const std::string& id) = 0; virtual bool existsMMKV(const std::string& id) = 0; + virtual bool backupMMKV(const BackupMMKVOptions& options) = 0; + virtual bool restoreMMKV(const RestoreMMKVOptions& options) = 0; + virtual double backupAllMMKV(const BackupAllMMKVOptions& options) = 0; + virtual double restoreAllMMKV(const RestoreAllMMKVOptions& options) = 0; protected: // Hybrid Setup diff --git a/packages/react-native-mmkv/nitrogen/generated/shared/c++/HybridMMKVSpec.cpp b/packages/react-native-mmkv/nitrogen/generated/shared/c++/HybridMMKVSpec.cpp index 34ee7034..76900d93 100644 --- a/packages/react-native-mmkv/nitrogen/generated/shared/c++/HybridMMKVSpec.cpp +++ b/packages/react-native-mmkv/nitrogen/generated/shared/c++/HybridMMKVSpec.cpp @@ -33,6 +33,8 @@ namespace margelo::nitro::mmkv { prototype.registerHybridMethod("encrypt", &HybridMMKVSpec::encrypt); prototype.registerHybridMethod("decrypt", &HybridMMKVSpec::decrypt); prototype.registerHybridMethod("trim", &HybridMMKVSpec::trim); + prototype.registerHybridMethod("backupToDirectory", &HybridMMKVSpec::backupToDirectory); + prototype.registerHybridMethod("restoreFromDirectory", &HybridMMKVSpec::restoreFromDirectory); prototype.registerHybridMethod("addOnValueChangedListener", &HybridMMKVSpec::addOnValueChangedListener); prototype.registerHybridMethod("importAllFrom", &HybridMMKVSpec::importAllFrom); }); diff --git a/packages/react-native-mmkv/nitrogen/generated/shared/c++/HybridMMKVSpec.hpp b/packages/react-native-mmkv/nitrogen/generated/shared/c++/HybridMMKVSpec.hpp index df38323e..eedf1677 100644 --- a/packages/react-native-mmkv/nitrogen/generated/shared/c++/HybridMMKVSpec.hpp +++ b/packages/react-native-mmkv/nitrogen/generated/shared/c++/HybridMMKVSpec.hpp @@ -80,6 +80,8 @@ namespace margelo::nitro::mmkv { virtual void encrypt(const std::string& key, std::optional encryptionType) = 0; virtual void decrypt() = 0; virtual void trim() = 0; + virtual bool backupToDirectory(const std::string& destinationDirectory) = 0; + virtual bool restoreFromDirectory(const std::string& sourceDirectory) = 0; virtual Listener addOnValueChangedListener(const std::function& onValueChanged) = 0; virtual double importAllFrom(const std::shared_ptr& other) = 0; diff --git a/packages/react-native-mmkv/nitrogen/generated/shared/c++/RestoreAllMMKVOptions.hpp b/packages/react-native-mmkv/nitrogen/generated/shared/c++/RestoreAllMMKVOptions.hpp new file mode 100644 index 00000000..992dfd7e --- /dev/null +++ b/packages/react-native-mmkv/nitrogen/generated/shared/c++/RestoreAllMMKVOptions.hpp @@ -0,0 +1,88 @@ +/// +/// RestoreAllMMKVOptions.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + +#include +#include + +namespace margelo::nitro::mmkv { + + /** + * A struct which can be represented as a JavaScript object (RestoreAllMMKVOptions). + */ + struct RestoreAllMMKVOptions final { + public: + std::string sourceDirectory SWIFT_PRIVATE; + std::optional rootPath SWIFT_PRIVATE; + + public: + RestoreAllMMKVOptions() = default; + explicit RestoreAllMMKVOptions(std::string sourceDirectory, std::optional rootPath): sourceDirectory(sourceDirectory), rootPath(rootPath) {} + + public: + friend bool operator==(const RestoreAllMMKVOptions& lhs, const RestoreAllMMKVOptions& rhs) = default; + }; + +} // namespace margelo::nitro::mmkv + +namespace margelo::nitro { + + // C++ RestoreAllMMKVOptions <> JS RestoreAllMMKVOptions (object) + template <> + struct JSIConverter final { + static inline margelo::nitro::mmkv::RestoreAllMMKVOptions fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return margelo::nitro::mmkv::RestoreAllMMKVOptions( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "sourceDirectory"))), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "rootPath"))) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::mmkv::RestoreAllMMKVOptions& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "sourceDirectory"), JSIConverter::toJSI(runtime, arg.sourceDirectory)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "rootPath"), JSIConverter>::toJSI(runtime, arg.rootPath)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!nitro::isPlainObject(runtime, obj)) { + return false; + } + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "sourceDirectory")))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "rootPath")))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/packages/react-native-mmkv/nitrogen/generated/shared/c++/RestoreMMKVOptions.hpp b/packages/react-native-mmkv/nitrogen/generated/shared/c++/RestoreMMKVOptions.hpp new file mode 100644 index 00000000..72e0d431 --- /dev/null +++ b/packages/react-native-mmkv/nitrogen/generated/shared/c++/RestoreMMKVOptions.hpp @@ -0,0 +1,92 @@ +/// +/// RestoreMMKVOptions.hpp +/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. +/// https://github.com/mrousavy/nitro +/// Copyright © Marc Rousavy @ Margelo +/// + +#pragma once + +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif +#if __has_include() +#include +#else +#error NitroModules cannot be found! Are you sure you installed NitroModules properly? +#endif + + + +#include +#include + +namespace margelo::nitro::mmkv { + + /** + * A struct which can be represented as a JavaScript object (RestoreMMKVOptions). + */ + struct RestoreMMKVOptions final { + public: + std::string id SWIFT_PRIVATE; + std::string sourceDirectory SWIFT_PRIVATE; + std::optional rootPath SWIFT_PRIVATE; + + public: + RestoreMMKVOptions() = default; + explicit RestoreMMKVOptions(std::string id, std::string sourceDirectory, std::optional rootPath): id(id), sourceDirectory(sourceDirectory), rootPath(rootPath) {} + + public: + friend bool operator==(const RestoreMMKVOptions& lhs, const RestoreMMKVOptions& rhs) = default; + }; + +} // namespace margelo::nitro::mmkv + +namespace margelo::nitro { + + // C++ RestoreMMKVOptions <> JS RestoreMMKVOptions (object) + template <> + struct JSIConverter final { + static inline margelo::nitro::mmkv::RestoreMMKVOptions fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) { + jsi::Object obj = arg.asObject(runtime); + return margelo::nitro::mmkv::RestoreMMKVOptions( + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "id"))), + JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "sourceDirectory"))), + JSIConverter>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "rootPath"))) + ); + } + static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::mmkv::RestoreMMKVOptions& arg) { + jsi::Object obj(runtime); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "id"), JSIConverter::toJSI(runtime, arg.id)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "sourceDirectory"), JSIConverter::toJSI(runtime, arg.sourceDirectory)); + obj.setProperty(runtime, PropNameIDCache::get(runtime, "rootPath"), JSIConverter>::toJSI(runtime, arg.rootPath)); + return obj; + } + static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) { + if (!value.isObject()) { + return false; + } + jsi::Object obj = value.getObject(runtime); + if (!nitro::isPlainObject(runtime, obj)) { + return false; + } + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "id")))) return false; + if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "sourceDirectory")))) return false; + if (!JSIConverter>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "rootPath")))) return false; + return true; + } + }; + +} // namespace margelo::nitro diff --git a/packages/react-native-mmkv/src/backupAllMMKV/backupAllMMKV.ts b/packages/react-native-mmkv/src/backupAllMMKV/backupAllMMKV.ts new file mode 100644 index 00000000..d90a815b --- /dev/null +++ b/packages/react-native-mmkv/src/backupAllMMKV/backupAllMMKV.ts @@ -0,0 +1,12 @@ +import { getMMKVFactory } from '../getMMKVFactory' +import { isTest } from '../isTest' +import type { BackupAllMMKVOptions } from '../specs/MMKVFactory.nitro' + +export function backupAllMMKV(options: BackupAllMMKVOptions): number { + if (isTest()) { + return 0 + } + + const factory = getMMKVFactory() + return factory.backupAllMMKV(options) +} diff --git a/packages/react-native-mmkv/src/backupAllMMKV/backupAllMMKV.web.ts b/packages/react-native-mmkv/src/backupAllMMKV/backupAllMMKV.web.ts new file mode 100644 index 00000000..06f187db --- /dev/null +++ b/packages/react-native-mmkv/src/backupAllMMKV/backupAllMMKV.web.ts @@ -0,0 +1,5 @@ +import type { BackupAllMMKVOptions } from '../specs/MMKVFactory.nitro' + +export function backupAllMMKV(_options: BackupAllMMKVOptions): number { + throw new Error("MMKV: 'backupAllMMKV(..)' is not supported on Web!") +} diff --git a/packages/react-native-mmkv/src/backupMMKV/backupMMKV.ts b/packages/react-native-mmkv/src/backupMMKV/backupMMKV.ts new file mode 100644 index 00000000..3911cb1f --- /dev/null +++ b/packages/react-native-mmkv/src/backupMMKV/backupMMKV.ts @@ -0,0 +1,12 @@ +import { getMMKVFactory } from '../getMMKVFactory' +import { isTest } from '../isTest' +import type { BackupMMKVOptions } from '../specs/MMKVFactory.nitro' + +export function backupMMKV(options: BackupMMKVOptions): boolean { + if (isTest()) { + return true + } + + const factory = getMMKVFactory() + return factory.backupMMKV(options) +} diff --git a/packages/react-native-mmkv/src/backupMMKV/backupMMKV.web.ts b/packages/react-native-mmkv/src/backupMMKV/backupMMKV.web.ts new file mode 100644 index 00000000..60d345b6 --- /dev/null +++ b/packages/react-native-mmkv/src/backupMMKV/backupMMKV.web.ts @@ -0,0 +1,5 @@ +import type { BackupMMKVOptions } from '../specs/MMKVFactory.nitro' + +export function backupMMKV(_options: BackupMMKVOptions): boolean { + throw new Error("MMKV: 'backupMMKV(..)' is not supported on Web!") +} diff --git a/packages/react-native-mmkv/src/createMMKV/createMMKV.web.ts b/packages/react-native-mmkv/src/createMMKV/createMMKV.web.ts index 24e17e6d..0ccfd704 100644 --- a/packages/react-native-mmkv/src/createMMKV/createMMKV.web.ts +++ b/packages/react-native-mmkv/src/createMMKV/createMMKV.web.ts @@ -126,6 +126,12 @@ export function createMMKV( trim: () => { // no-op }, + backupToDirectory: () => { + throw new Error('`backupToDirectory(..)` is not supported on Web!') + }, + restoreFromDirectory: () => { + throw new Error('`restoreFromDirectory(..)` is not supported on Web!') + }, dispose: () => {}, equals: () => false, name: 'MMKV', diff --git a/packages/react-native-mmkv/src/createMMKV/createMockMMKV.ts b/packages/react-native-mmkv/src/createMMKV/createMockMMKV.ts index 33c9ace7..96b1db2d 100644 --- a/packages/react-native-mmkv/src/createMMKV/createMockMMKV.ts +++ b/packages/react-native-mmkv/src/createMMKV/createMockMMKV.ts @@ -80,6 +80,14 @@ export function createMockMMKV( trim: () => { // no-op }, + backupToDirectory: () => { + console.warn('Backup is not supported in mocked MMKV instances!') + return true + }, + restoreFromDirectory: () => { + console.warn('Restore is not supported in mocked MMKV instances!') + return true + }, name: 'MMKV', dispose: () => {}, equals: () => { diff --git a/packages/react-native-mmkv/src/index.ts b/packages/react-native-mmkv/src/index.ts index f059d8aa..868fd598 100644 --- a/packages/react-native-mmkv/src/index.ts +++ b/packages/react-native-mmkv/src/index.ts @@ -1,6 +1,13 @@ // All types export type { MMKV } from './specs/MMKV.nitro' -export type { Configuration, Mode } from './specs/MMKVFactory.nitro' +export type { + BackupAllMMKVOptions, + BackupMMKVOptions, + Configuration, + Mode, + RestoreAllMMKVOptions, + RestoreMMKVOptions, +} from './specs/MMKVFactory.nitro' // The create function export { createMMKV } from './createMMKV/createMMKV' @@ -9,6 +16,12 @@ export { createMMKV } from './createMMKV/createMMKV' export { existsMMKV } from './existsMMKV/existsMMKV' export { deleteMMKV } from './deleteMMKV/deleteMMKV' +// Backup + Restore +export { backupMMKV } from './backupMMKV/backupMMKV' +export { restoreMMKV } from './restoreMMKV/restoreMMKV' +export { backupAllMMKV } from './backupAllMMKV/backupAllMMKV' +export { restoreAllMMKV } from './restoreAllMMKV/restoreAllMMKV' + // All the hooks export { useMMKV } from './hooks/useMMKV' export { useMMKVBoolean } from './hooks/useMMKVBoolean' diff --git a/packages/react-native-mmkv/src/restoreAllMMKV/restoreAllMMKV.ts b/packages/react-native-mmkv/src/restoreAllMMKV/restoreAllMMKV.ts new file mode 100644 index 00000000..72342ca7 --- /dev/null +++ b/packages/react-native-mmkv/src/restoreAllMMKV/restoreAllMMKV.ts @@ -0,0 +1,12 @@ +import { getMMKVFactory } from '../getMMKVFactory' +import { isTest } from '../isTest' +import type { RestoreAllMMKVOptions } from '../specs/MMKVFactory.nitro' + +export function restoreAllMMKV(options: RestoreAllMMKVOptions): number { + if (isTest()) { + return 0 + } + + const factory = getMMKVFactory() + return factory.restoreAllMMKV(options) +} diff --git a/packages/react-native-mmkv/src/restoreAllMMKV/restoreAllMMKV.web.ts b/packages/react-native-mmkv/src/restoreAllMMKV/restoreAllMMKV.web.ts new file mode 100644 index 00000000..dc80979c --- /dev/null +++ b/packages/react-native-mmkv/src/restoreAllMMKV/restoreAllMMKV.web.ts @@ -0,0 +1,5 @@ +import type { RestoreAllMMKVOptions } from '../specs/MMKVFactory.nitro' + +export function restoreAllMMKV(_options: RestoreAllMMKVOptions): number { + throw new Error("MMKV: 'restoreAllMMKV(..)' is not supported on Web!") +} diff --git a/packages/react-native-mmkv/src/restoreMMKV/restoreMMKV.ts b/packages/react-native-mmkv/src/restoreMMKV/restoreMMKV.ts new file mode 100644 index 00000000..beb2801e --- /dev/null +++ b/packages/react-native-mmkv/src/restoreMMKV/restoreMMKV.ts @@ -0,0 +1,12 @@ +import { getMMKVFactory } from '../getMMKVFactory' +import { isTest } from '../isTest' +import type { RestoreMMKVOptions } from '../specs/MMKVFactory.nitro' + +export function restoreMMKV(options: RestoreMMKVOptions): boolean { + if (isTest()) { + return true + } + + const factory = getMMKVFactory() + return factory.restoreMMKV(options) +} diff --git a/packages/react-native-mmkv/src/restoreMMKV/restoreMMKV.web.ts b/packages/react-native-mmkv/src/restoreMMKV/restoreMMKV.web.ts new file mode 100644 index 00000000..6eb32e7b --- /dev/null +++ b/packages/react-native-mmkv/src/restoreMMKV/restoreMMKV.web.ts @@ -0,0 +1,5 @@ +import type { RestoreMMKVOptions } from '../specs/MMKVFactory.nitro' + +export function restoreMMKV(_options: RestoreMMKVOptions): boolean { + throw new Error("MMKV: 'restoreMMKV(..)' is not supported on Web!") +} diff --git a/packages/react-native-mmkv/src/specs/MMKV.nitro.ts b/packages/react-native-mmkv/src/specs/MMKV.nitro.ts index f97eaaa1..8dc2de23 100644 --- a/packages/react-native-mmkv/src/specs/MMKV.nitro.ts +++ b/packages/react-native-mmkv/src/specs/MMKV.nitro.ts @@ -133,6 +133,16 @@ export interface MMKV extends HybridObject<{ ios: 'c++'; android: 'c++' }> { * In most applications, this is not needed at all. */ trim(): void + /** + * Back up this MMKV instance to the given destination directory. + * @returns true if the backup was successful, false otherwise. + */ + backupToDirectory(destinationDirectory: string): boolean + /** + * Restore this MMKV instance from the given source directory. + * @returns true if the restore was successful, false otherwise. + */ + restoreFromDirectory(sourceDirectory: string): boolean /** * Adds a value changed listener. The Listener will be called whenever any value * in this storage instance changes (set or delete). diff --git a/packages/react-native-mmkv/src/specs/MMKVFactory.nitro.ts b/packages/react-native-mmkv/src/specs/MMKVFactory.nitro.ts index 734f1558..a44fcfa6 100644 --- a/packages/react-native-mmkv/src/specs/MMKVFactory.nitro.ts +++ b/packages/react-native-mmkv/src/specs/MMKVFactory.nitro.ts @@ -96,6 +96,66 @@ export interface Configuration { compareBeforeSet?: boolean } +export interface BackupMMKVOptions { + /** + * The MMKV instance ID to back up. + */ + id: string + /** + * The directory to write the backup files to. + */ + destinationDirectory: string + /** + * The root path where the MMKV instance is stored. + * + * @default undefined + */ + rootPath?: string +} + +export interface RestoreMMKVOptions { + /** + * The MMKV instance ID to restore. + */ + id: string + /** + * The directory to restore the backup files from. + */ + sourceDirectory: string + /** + * The root path where the MMKV instance should be restored. + * + * @default undefined + */ + rootPath?: string +} + +export interface BackupAllMMKVOptions { + /** + * The directory to write the backup files to. + */ + destinationDirectory: string + /** + * The root path where the MMKV instances are stored. + * + * @default undefined + */ + rootPath?: string +} + +export interface RestoreAllMMKVOptions { + /** + * The directory to restore the backup files from. + */ + sourceDirectory: string + /** + * The root path where the MMKV instances should be restored. + * + * @default undefined + */ + rootPath?: string +} + export interface MMKVFactory extends HybridObject<{ ios: 'c++' android: 'c++' @@ -123,6 +183,28 @@ export interface MMKVFactory extends HybridObject<{ */ existsMMKV(id: string): boolean + /** + * Back up one MMKV instance to the given destination directory. + */ + backupMMKV(options: BackupMMKVOptions): boolean + + /** + * Restore one MMKV instance from the given source directory. + */ + restoreMMKV(options: RestoreMMKVOptions): boolean + + /** + * Back up all MMKV instances to the given destination directory. + * @returns the number of backed-up instances. + */ + backupAllMMKV(options: BackupAllMMKVOptions): number + + /** + * Restore all MMKV instances from the given source directory. + * @returns the number of restored instances. + */ + restoreAllMMKV(options: RestoreAllMMKVOptions): number + /** * Get the default MMKV instance's ID. * @default 'mmkv.default'