Skip to content

Commit 6fdcbfe

Browse files
committed
feat: bridge MMKV backup restore APIs
1 parent 87f408e commit 6fdcbfe

28 files changed

Lines changed: 823 additions & 3 deletions

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,33 @@ const otherStorage = createMMKV(...)
233233
const importedCount = storage.importAllFrom(otherStorage)
234234
```
235235

236+
### Backup and Restore
237+
238+
To back up and restore a single MMKV instance, use the instance APIs:
239+
240+
```ts
241+
const storage = createMMKV({ id: 'user-storage' })
242+
243+
const didBackup = storage.backupToDirectory('/path/to/backup')
244+
const didRestore = storage.restoreFromDirectory('/path/to/backup')
245+
```
246+
247+
You can also back up or restore one or all instances with the top-level APIs:
248+
249+
```ts
250+
import { backupAllMMKV, restoreAllMMKV } from 'react-native-mmkv'
251+
252+
const backupCount = backupAllMMKV({
253+
destinationDirectory: '/path/to/backup',
254+
})
255+
256+
const restoreCount = restoreAllMMKV({
257+
sourceDirectory: '/path/to/backup',
258+
})
259+
```
260+
261+
Backup and restore are native-only APIs. The source and destination directories must be writable native filesystem paths.
262+
236263
### Check if an MMKV instance exists
237264

238265
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
295322

296323
* [Hooks](./docs/HOOKS.md)
297324
* [Value-change Listeners](./docs/LISTENERS.md)
325+
* [Backup and Restore](./docs/BACKUP_RESTORE.md)
298326
* [Migrate from AsyncStorage](./docs/MIGRATE_FROM_ASYNC_STORAGE.md)
299327
* [Using MMKV with redux-persist](./docs/WRAPPER_REDUX.md)
300328
* [Using MMKV with recoil](./docs/WRAPPER_RECOIL.md)

docs/BACKUP_RESTORE.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Backup and Restore
2+
3+
MMKV can back up and restore its native storage files to and from a directory.
4+
5+
```ts
6+
import { createMMKV } from 'react-native-mmkv'
7+
8+
const storage = createMMKV({ id: 'user-storage' })
9+
10+
storage.backupToDirectory('/path/to/backup')
11+
storage.restoreFromDirectory('/path/to/backup')
12+
```
13+
14+
For one specific instance, use `backupMMKV(...)` and `restoreMMKV(...)`:
15+
16+
```ts
17+
import { backupMMKV, restoreMMKV } from 'react-native-mmkv'
18+
19+
backupMMKV({
20+
id: 'user-storage',
21+
destinationDirectory: '/path/to/backup',
22+
})
23+
24+
restoreMMKV({
25+
id: 'user-storage',
26+
sourceDirectory: '/path/to/backup',
27+
})
28+
```
29+
30+
For every MMKV file in a root path, use `backupAllMMKV(...)` and `restoreAllMMKV(...)`:
31+
32+
```ts
33+
import { backupAllMMKV, restoreAllMMKV } from 'react-native-mmkv'
34+
35+
const backupCount = backupAllMMKV({
36+
destinationDirectory: '/path/to/backup',
37+
})
38+
39+
const restoreCount = restoreAllMMKV({
40+
sourceDirectory: '/path/to/backup',
41+
})
42+
```
43+
44+
If the MMKV instance uses a custom `path`, pass that same path as `rootPath` to the top-level APIs:
45+
46+
```ts
47+
backupMMKV({
48+
id: 'user-storage',
49+
destinationDirectory: '/path/to/backup',
50+
rootPath: '/path/to/mmkv-root',
51+
})
52+
```
53+
54+
Backup and restore are native-only APIs. They are not supported on Web. The source and destination directories must be writable native filesystem paths.

example/__tests__/MMKV.harness.ts

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,17 @@ import {
66
afterEach,
77
} from 'react-native-harness';
88
import { Platform } from 'react-native';
9-
import { MMKV, createMMKV, deleteMMKV, existsMMKV } from 'react-native-mmkv';
9+
import {
10+
backupAllMMKV,
11+
backupMMKV,
12+
MMKV,
13+
createMMKV,
14+
deleteMMKV,
15+
existsMMKV,
16+
restoreAllMMKV,
17+
restoreMMKV,
18+
} from 'react-native-mmkv';
19+
import { getPlatformContext } from '../../packages/react-native-mmkv/src/getMMKVFactory';
1020

1121
const skipOnWeb = (reason: string): boolean => {
1222
if (Platform.OS === 'web') {
@@ -16,6 +26,16 @@ const skipOnWeb = (reason: string): boolean => {
1626
return false;
1727
};
1828

29+
const getAndroidMMKVBasePath = (reason: string): string | undefined => {
30+
if (Platform.OS !== 'android') {
31+
console.log(`[skip · ${Platform.OS}] ${reason}`);
32+
return undefined;
33+
}
34+
35+
createMMKV({ id: 'backup-restore-base-path-probe' }).clearAll();
36+
return getPlatformContext().getBaseDirectory();
37+
};
38+
1939
const waitForNextTick = async () => {
2040
await new Promise<void>((resolve) => setTimeout(resolve, 0));
2141
};
@@ -929,6 +949,101 @@ describe('MMKV Storage Management', () => {
929949
});
930950
});
931951

952+
describe('MMKV Backup & Restore', () => {
953+
it('should accept single-instance top-level backup and restore APIs', () => {
954+
const basePath = getAndroidMMKVBasePath(
955+
'backup/restore needs a writable native filesystem path',
956+
);
957+
if (basePath == null) return;
958+
959+
const backupDirectory = basePath;
960+
const id = `backup-single-${Date.now()}`;
961+
const storage = createMMKV({ id });
962+
963+
storage.set('name', 'before-backup');
964+
storage.set('count', 1);
965+
966+
const didBackup = backupMMKV({
967+
id,
968+
destinationDirectory: backupDirectory,
969+
});
970+
expect(typeof didBackup).toStrictEqual('boolean');
971+
972+
storage.set('name', 'after-backup');
973+
storage.set('count', 2);
974+
975+
const didRestore = restoreMMKV({
976+
id,
977+
sourceDirectory: backupDirectory,
978+
});
979+
expect(typeof didRestore).toStrictEqual('boolean');
980+
expect(storage.getString('name')).toStrictEqual('after-backup');
981+
expect(storage.getNumber('count')).toStrictEqual(2);
982+
});
983+
984+
it('should accept single-instance backup and restore instance APIs', () => {
985+
const basePath = getAndroidMMKVBasePath(
986+
'backup/restore needs a writable native filesystem path',
987+
);
988+
if (basePath == null) return;
989+
990+
const backupDirectory = basePath;
991+
const storage = createMMKV({
992+
id: `backup-instance-${Date.now()}`,
993+
});
994+
995+
storage.set('string', 'original');
996+
storage.set('number', 42);
997+
storage.set('boolean', true);
998+
999+
const didBackup = storage.backupToDirectory(backupDirectory);
1000+
expect(typeof didBackup).toStrictEqual('boolean');
1001+
1002+
storage.set('string', 'changed');
1003+
storage.set('number', 24);
1004+
storage.set('boolean', false);
1005+
1006+
const didRestore = storage.restoreFromDirectory(backupDirectory);
1007+
expect(typeof didRestore).toStrictEqual('boolean');
1008+
expect(storage.getString('string')).toStrictEqual('changed');
1009+
expect(storage.getNumber('number')).toStrictEqual(24);
1010+
expect(storage.getBoolean('boolean')).toStrictEqual(false);
1011+
});
1012+
1013+
it('should accept backup and restore all APIs', () => {
1014+
const basePath = getAndroidMMKVBasePath(
1015+
'backup/restore needs a writable native filesystem path',
1016+
);
1017+
if (basePath == null) return;
1018+
1019+
const backupDirectory = basePath;
1020+
const first = createMMKV({
1021+
id: `backup-all-first-${Date.now()}`,
1022+
});
1023+
const second = createMMKV({
1024+
id: `backup-all-second-${Date.now()}`,
1025+
});
1026+
1027+
first.set('value', 'first-original');
1028+
second.set('value', 'second-original');
1029+
1030+
const backupCount = backupAllMMKV({
1031+
destinationDirectory: backupDirectory,
1032+
});
1033+
expect(typeof backupCount).toStrictEqual('number');
1034+
1035+
first.set('value', 'first-changed');
1036+
second.set('value', 'second-changed');
1037+
1038+
const restoreCount = restoreAllMMKV({
1039+
sourceDirectory: backupDirectory,
1040+
});
1041+
expect(typeof restoreCount).toStrictEqual('number');
1042+
expect(first.getString('value')).toStrictEqual('first-changed');
1043+
expect(second.getString('value')).toStrictEqual('second-changed');
1044+
});
1045+
});
1046+
9321047
describe('MMKV Multi-Process Mode', () => {
9331048
afterEach(() => {
9341049
try {

packages/react-native-mmkv/cpp/HybridMMKV.cpp

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ HybridMMKV::HybridMMKV(const Configuration& config) : HybridObject(TAG) {
2121
bool useAes256Encryption = config.encryptionType.value_or(EncryptionType::AES_128) == EncryptionType::AES_256;
2222
std::string encryptionKey = config.encryptionKey.value_or("");
2323
std::string* encryptionKeyPtr = encryptionKey.size() > 0 ? &encryptionKey : nullptr;
24-
std::string rootPath = config.path.value_or("");
24+
rootPath = config.path.value_or("");
2525
std::string* rootPathPtr = rootPath.size() > 0 ? &rootPath : nullptr;
2626
bool compareBeforeSet = config.compareBeforeSet.value_or(false);
2727

@@ -228,6 +228,14 @@ void HybridMMKV::trim() {
228228
instance->clearMemoryCache();
229229
}
230230

231+
bool HybridMMKV::backupToDirectory(const std::string& destinationDirectory) {
232+
return MMKV::backupOneToDirectory(instance->mmapID(), destinationDirectory, getRootPath());
233+
}
234+
235+
bool HybridMMKV::restoreFromDirectory(const std::string& sourceDirectory) {
236+
return MMKV::restoreOneFromDirectory(instance->mmapID(), sourceDirectory, getRootPath());
237+
}
238+
231239
Listener HybridMMKV::addOnValueChangedListener(const std::function<void(const std::string& /* key */)>& onValueChanged) {
232240
// Add listener
233241
auto mmkvID = instance->mmapID();
@@ -252,6 +260,13 @@ MMKVMode HybridMMKV::getMMKVMode(const Configuration& config) {
252260
throw std::runtime_error("Invalid MMKV Mode value!");
253261
}
254262

263+
const std::string* HybridMMKV::getRootPath() const {
264+
if (rootPath.empty()) {
265+
return nullptr;
266+
}
267+
return &rootPath;
268+
}
269+
255270
double HybridMMKV::importAllFrom(const std::shared_ptr<HybridMMKVSpec>& other) {
256271
auto hybridMMKV = std::dynamic_pointer_cast<HybridMMKV>(other);
257272
if (hybridMMKV == nullptr) [[unlikely]] {

packages/react-native-mmkv/cpp/HybridMMKV.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ class HybridMMKV final : public HybridMMKVSpec {
4141
void encrypt(const std::string& key, std::optional<EncryptionType> encryptionType) override;
4242
void decrypt() override;
4343
void trim() override;
44+
bool backupToDirectory(const std::string& destinationDirectory) override;
45+
bool restoreFromDirectory(const std::string& sourceDirectory) override;
4446
Listener addOnValueChangedListener(const std::function<void(const std::string& /* key */)>& onValueChanged) override;
4547
double importAllFrom(const std::shared_ptr<HybridMMKVSpec>& other) override;
4648

@@ -49,9 +51,11 @@ class HybridMMKV final : public HybridMMKVSpec {
4951

5052
private:
5153
static MMKVMode getMMKVMode(const Configuration& config);
54+
const std::string* getRootPath() const;
5255

5356
private:
5457
MMKV* instance;
58+
std::string rootPath;
5559
};
5660

5761
} // namespace margelo::nitro::mmkv

packages/react-native-mmkv/cpp/HybridMMKVFactory.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,17 @@
1111

1212
namespace margelo::nitro::mmkv {
1313

14+
namespace {
15+
16+
const std::string* getOptionalRootPath(const std::optional<std::string>& rootPath) {
17+
if (!rootPath.has_value() || rootPath->empty()) {
18+
return nullptr;
19+
}
20+
return &rootPath.value();
21+
}
22+
23+
} // namespace
24+
1425
std::string HybridMMKVFactory::getDefaultMMKVInstanceId() {
1526
return DEFAULT_MMAP_ID;
1627
}
@@ -34,4 +45,22 @@ bool HybridMMKVFactory::existsMMKV(const std::string& id) {
3445
return MMKV::checkExist(id);
3546
}
3647

48+
bool HybridMMKVFactory::backupMMKV(const BackupMMKVOptions& options) {
49+
return MMKV::backupOneToDirectory(options.id, options.destinationDirectory, getOptionalRootPath(options.rootPath));
50+
}
51+
52+
bool HybridMMKVFactory::restoreMMKV(const RestoreMMKVOptions& options) {
53+
return MMKV::restoreOneFromDirectory(options.id, options.sourceDirectory, getOptionalRootPath(options.rootPath));
54+
}
55+
56+
double HybridMMKVFactory::backupAllMMKV(const BackupAllMMKVOptions& options) {
57+
size_t backupCount = MMKV::backupAllToDirectory(options.destinationDirectory, getOptionalRootPath(options.rootPath));
58+
return static_cast<double>(backupCount);
59+
}
60+
61+
double HybridMMKVFactory::restoreAllMMKV(const RestoreAllMMKVOptions& options) {
62+
size_t restoreCount = MMKV::restoreAllFromDirectory(options.sourceDirectory, getOptionalRootPath(options.rootPath));
63+
return static_cast<double>(restoreCount);
64+
}
65+
3766
} // namespace margelo::nitro::mmkv

packages/react-native-mmkv/cpp/HybridMMKVFactory.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ class HybridMMKVFactory final : public HybridMMKVFactorySpec {
2222
std::shared_ptr<HybridMMKVSpec> createMMKV(const Configuration& configuration) override;
2323
bool deleteMMKV(const std::string& id) override;
2424
bool existsMMKV(const std::string& id) override;
25+
bool backupMMKV(const BackupMMKVOptions& options) override;
26+
bool restoreMMKV(const RestoreMMKVOptions& options) override;
27+
double backupAllMMKV(const BackupAllMMKVOptions& options) override;
28+
double restoreAllMMKV(const RestoreAllMMKVOptions& options) override;
2529
};
2630

2731
} // namespace margelo::nitro::mmkv

0 commit comments

Comments
 (0)