From 80efe7026cf48ec4563faa476d6a847ce9e9c866 Mon Sep 17 00:00:00 2001 From: Amsyar Rasyiq <82711525+amsyarasyiq@users.noreply.github.com> Date: Sun, 17 Dec 2023 10:09:12 +0800 Subject: [PATCH] [Lib > Storage] Move from MMKVManager to FS (#206) * init migration * fixes --- src/def.d.ts | 2 ++ src/lib/metro/common.ts | 2 +- src/lib/plugins.ts | 6 ++-- src/lib/storage/backends.ts | 72 +++++++++++++++++++++++++++++++------ 4 files changed, 68 insertions(+), 14 deletions(-) diff --git a/src/def.d.ts b/src/def.d.ts index ccc57ab..9d4c574 100644 --- a/src/def.d.ts +++ b/src/def.d.ts @@ -292,11 +292,13 @@ interface FileManager { * @returns Promise that resolves to path of the file once it got written */ writeFile(storageDir: "cache" | "documents", path: string, data: string, encoding: "base64" | "utf8"): Promise; + removeFile(storageDir: "cache" | "documents", path: string): Promise; getConstants: () => { /** * The path the `documents` storage dir (see {@link writeFile}) represents. */ DocumentsDirPath: string; + CacheDirPath: string; }; /** * Will apparently cease to exist some time in the future so please use {@link getConstants} instead. diff --git a/src/lib/metro/common.ts b/src/lib/metro/common.ts index 3ad4a3c..5724c93 100644 --- a/src/lib/metro/common.ts +++ b/src/lib/metro/common.ts @@ -29,7 +29,7 @@ export const constants = findByProps("Fonts", "Permissions"); export const channels = findByProps("getVoiceChannelId"); export const i18n = findByProps("Messages"); export const url = findByProps("openURL", "openDeeplink"); -export const toasts = find(m => m.open && m.close && !m.startDrag && !m.init && !m.openReplay && !m.setAlwaysOnTop); +export const toasts = find(m => m.open && m.close && !m.startDrag && !m.init && !m.openReplay && !m.setAlwaysOnTop && !m.setAccountFlag); // Compatible with pre-204201 versions since createThemedStyleSheet is undefined. export const stylesheet = { diff --git a/src/lib/plugins.ts b/src/lib/plugins.ts index d44236a..bc81bce 100644 --- a/src/lib/plugins.ts +++ b/src/lib/plugins.ts @@ -1,6 +1,6 @@ import { PluginManifest, Plugin } from "@types"; import { safeFetch } from "@lib/utils"; -import { awaitSyncWrapper, createMMKVBackend, createStorage, wrapSync } from "@lib/storage"; +import { awaitSyncWrapper, createMMKVBackend, createStorage, purgeStorage, wrapSync } from "@lib/storage"; import { MMKVManager } from "@lib/native"; import { allSettled } from "@lib/polyfills"; import logger, { logModule } from "@lib/logger"; @@ -117,12 +117,12 @@ export function stopPlugin(id: string, disable = true) { disable && (plugin.enabled = false); } -export function removePlugin(id: string) { +export async function removePlugin(id: string) { if (!id.endsWith("/")) id += "/"; const plugin = plugins[id]; if (plugin.enabled) stopPlugin(id); - MMKVManager.removeItem(id); delete plugins[id]; + await purgeStorage(id); } export async function initPlugins() { diff --git a/src/lib/storage/backends.ts b/src/lib/storage/backends.ts index 546cb0f..8a544c1 100644 --- a/src/lib/storage/backends.ts +++ b/src/lib/storage/backends.ts @@ -1,25 +1,77 @@ import { StorageBackend } from "@types"; -import { ReactNative as RN } from "@metro/common"; import { MMKVManager, FileManager } from "@lib/native"; +import { ReactNative as RN } from "@metro/common"; -export const createMMKVBackend = (store: string): StorageBackend => ({ - get: async () => JSON.parse((await MMKVManager.getItem(store)) ?? "{}"), - set: (data) => MMKVManager.setItem(store, JSON.stringify(data)), +const ILLEGAL_CHARS_REGEX = /[<>:"\/\\|?*]/g; + +const filePathFixer = (file: string): string => RN.Platform.select({ + default: file, + ios: FileManager.saveFileToGallery ? file : `Documents/${file}`, }); -export const createFileBackend = (file: string): StorageBackend => { - const filePathFixer: (file: string) => string = RN.Platform.select({ - default: (f) => f, - ios: (f) => FileManager.saveFileToGallery ? f : `Documents/${f}`, - }); +const getMMKVPath = (name: string): string => { + if (ILLEGAL_CHARS_REGEX.test(name)) { + // Replace forbidden characters with hyphens + name = name.replace(ILLEGAL_CHARS_REGEX, '-').replace(/-+/g, '-'); + } + return `vd_mmkv/${name}`; +} + +export const purgeStorage = async (store: string) => { + if (await MMKVManager.getItem(store)) { + MMKVManager.removeItem(store); + } + + const mmkvPath = getMMKVPath(store); + if (await FileManager.fileExists(`${FileManager.getConstants().DocumentsDirPath}/${mmkvPath}`)) { + await FileManager.removeFile?.("documents", mmkvPath); + } +} + +export const createMMKVBackend = (store: string) => { + const mmkvPath = getMMKVPath(store); + return createFileBackend(mmkvPath, (async () => { + try { + const path = `${FileManager.getConstants().DocumentsDirPath}/${mmkvPath}`; + if (await FileManager.fileExists(path)) return; + + let oldData = await MMKVManager.getItem(store) ?? "{}"; + + // From the testing on Android, it seems to return this if the data is too large + if (oldData === "!!LARGE_VALUE!!") { + const cachePath = `${FileManager.getConstants().CacheDirPath}/mmkv/${store}`; + if (await FileManager.fileExists(cachePath)) { + oldData = await FileManager.readFile(cachePath, "utf8") + } else { + console.log(`${store}: Experienced data loss :(`); + oldData = "{}"; + } + } + + await FileManager.writeFile("documents", filePathFixer(mmkvPath), oldData, "utf8"); + if (await MMKVManager.getItem(store) !== null) { + MMKVManager.removeItem(store); + console.log(`Successfully migrated ${store} store from MMKV storage to fs`); + } + } catch (err) { + console.error("Failed to migrate to fs from MMKVManager ", err) + } + })()); +} + +export const createFileBackend = (file: string, migratePromise?: Promise): StorageBackend => { let created: boolean; return { get: async () => { + await migratePromise; const path = `${FileManager.getConstants().DocumentsDirPath}/${file}`; if (!created && !(await FileManager.fileExists(path))) return (created = true), FileManager.writeFile("documents", filePathFixer(file), "{}", "utf8"); return JSON.parse(await FileManager.readFile(path, "utf8")); }, - set: async (data) => void await FileManager.writeFile("documents", filePathFixer(file), JSON.stringify(data), "utf8"), + set: async (data) => { + await migratePromise; + await FileManager.writeFile("documents", filePathFixer(file), JSON.stringify(data), "utf8"); + } }; };