Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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(...)`:
Expand Down Expand Up @@ -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)
Expand Down
54 changes: 54 additions & 0 deletions docs/BACKUP_RESTORE.md
Original file line number Diff line number Diff line change
@@ -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.
117 changes: 116 additions & 1 deletion example/__tests__/MMKV.harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand All @@ -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<void>((resolve) => setTimeout(resolve, 0));
};
Expand Down Expand Up @@ -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 {
Expand Down
17 changes: 16 additions & 1 deletion packages/react-native-mmkv/cpp/HybridMMKV.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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<void(const std::string& /* key */)>& onValueChanged) {
// Add listener
auto mmkvID = instance->mmapID();
Expand All @@ -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<HybridMMKVSpec>& other) {
auto hybridMMKV = std::dynamic_pointer_cast<HybridMMKV>(other);
if (hybridMMKV == nullptr) [[unlikely]] {
Expand Down
4 changes: 4 additions & 0 deletions packages/react-native-mmkv/cpp/HybridMMKV.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ class HybridMMKV final : public HybridMMKVSpec {
void encrypt(const std::string& key, std::optional<EncryptionType> 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<void(const std::string& /* key */)>& onValueChanged) override;
double importAllFrom(const std::shared_ptr<HybridMMKVSpec>& other) override;

Expand All @@ -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
29 changes: 29 additions & 0 deletions packages/react-native-mmkv/cpp/HybridMMKVFactory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@

namespace margelo::nitro::mmkv {

namespace {

const std::string* getOptionalRootPath(const std::optional<std::string>& rootPath) {
if (!rootPath.has_value() || rootPath->empty()) {
return nullptr;
}
return &rootPath.value();
}

} // namespace

std::string HybridMMKVFactory::getDefaultMMKVInstanceId() {
return DEFAULT_MMAP_ID;
}
Expand All @@ -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<double>(backupCount);
}

double HybridMMKVFactory::restoreAllMMKV(const RestoreAllMMKVOptions& options) {
size_t restoreCount = MMKV::restoreAllFromDirectory(options.sourceDirectory, getOptionalRootPath(options.rootPath));
return static_cast<double>(restoreCount);
}

} // namespace margelo::nitro::mmkv
4 changes: 4 additions & 0 deletions packages/react-native-mmkv/cpp/HybridMMKVFactory.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ class HybridMMKVFactory final : public HybridMMKVFactorySpec {
std::shared_ptr<HybridMMKVSpec> 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
Loading
Loading