diff --git a/packages/core/src/storage/FileSystem.ts b/packages/core/src/storage/FileSystem.ts index 9a5710835e..5317c53a17 100644 --- a/packages/core/src/storage/FileSystem.ts +++ b/packages/core/src/storage/FileSystem.ts @@ -16,4 +16,5 @@ export interface FileSystem { read(path: string): Promise delete(path: string): Promise downloadToFile(url: string, path: string, options?: DownloadToFileOptions): Promise + migrateWalletToCredoFolder(walletId: string): Promise } diff --git a/packages/core/src/storage/migration/updates.ts b/packages/core/src/storage/migration/updates.ts index ad06ad3178..d92f32b414 100644 --- a/packages/core/src/storage/migration/updates.ts +++ b/packages/core/src/storage/migration/updates.ts @@ -7,6 +7,7 @@ import { updateV0_2ToV0_3 } from './updates/0.2-0.3' import { updateV0_3ToV0_3_1 } from './updates/0.3-0.3.1' import { updateV0_3_1ToV0_4 } from './updates/0.3.1-0.4' import { updateV0_4ToV0_5 } from './updates/0.4-0.5' +import { updateV0_5ToV0_6 } from './updates/0.5-0.6' export const INITIAL_STORAGE_VERSION = '0.1' @@ -52,6 +53,11 @@ export const supportedUpdates = [ toVersion: '0.5', doUpdate: updateV0_4ToV0_5, }, + { + fromVersion: '0.5', + toVersion: '0.6', + doUpdate: updateV0_5ToV0_6, + }, ] as const // Current version is last toVersion from the supported updates diff --git a/packages/core/src/storage/migration/updates/0.5-0.6/index.ts b/packages/core/src/storage/migration/updates/0.5-0.6/index.ts new file mode 100644 index 0000000000..5b352080c4 --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.5-0.6/index.ts @@ -0,0 +1,7 @@ +import type { BaseAgent } from '../../../../agent/BaseAgent' + +import { migrateToCredoFolder } from './migrateToCredoFolder' + +export async function updateV0_5ToV0_6(agent: Agent): Promise { + await migrateToCredoFolder(agent) +} diff --git a/packages/core/src/storage/migration/updates/0.5-0.6/migrateToCredoFolder.ts b/packages/core/src/storage/migration/updates/0.5-0.6/migrateToCredoFolder.ts new file mode 100644 index 0000000000..a54a0ef72d --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.5-0.6/migrateToCredoFolder.ts @@ -0,0 +1,36 @@ +import type { BaseAgent } from '../../../../agent/BaseAgent' +import type { FileSystem } from '../../../FileSystem' + +import { InjectionSymbols } from '../../../../constants' +import { CredoError } from '../../../../error' + +/** + * Migrates the sqlite folder location from .afj to .credo in the storage directory in node and react native. + * + */ +export async function migrateToCredoFolder(agent: Agent) { + const walletId = agent.config.walletConfig?.id + + if (!walletId) { + throw new CredoError('Wallet id is required to migrate the wallet to .credo') + } + + // Adding type assertion to get the storage config + const storageConfig = agent.config.walletConfig?.storage as { + config?: { inMemory?: boolean } + } + + // If no storage config is provided, we set default as sqlite + // https://github.com/openwallet-foundation/credo-ts/blob/main/packages/askar/src/utils/askarWalletConfig.ts#L35 + // and we only migrate the data folder if the storage config is not set to inMemory + if (!storageConfig || (storageConfig.config && !storageConfig.config?.inMemory)) { + return + } + + agent.config.logger.info('Migrating data from .afj to .credo') + + const fileSystem = agent.dependencyManager.resolve(InjectionSymbols.FileSystem) + + await fileSystem.migrateWalletToCredoFolder(walletId) + agent.config.logger.info('Migration completed successfully') +} diff --git a/packages/node/src/NodeFileSystem.ts b/packages/node/src/NodeFileSystem.ts index eb8611d7c4..ac878aae47 100644 --- a/packages/node/src/NodeFileSystem.ts +++ b/packages/node/src/NodeFileSystem.ts @@ -8,7 +8,7 @@ import https from 'https' import { tmpdir, homedir } from 'os' import { dirname } from 'path' -const { access, readFile, writeFile, mkdir, rm, unlink, copyFile } = promises +const { access, readFile, writeFile, mkdir, rm, unlink, copyFile, cp } = promises export class NodeFileSystem implements FileSystem { public readonly dataPath @@ -19,16 +19,61 @@ export class NodeFileSystem implements FileSystem { * Create new NodeFileSystem class instance. * * @param baseDataPath The base path to use for reading and writing data files used within the framework. - * Files will be created under baseDataPath/.afj directory. If not specified, it will be set to homedir() + * Files will be created under baseDataPath/.credo directory. If not specified, it will be set to homedir() * @param baseCachePath The base path to use for reading and writing cache files used within the framework. - * Files will be created under baseCachePath/.afj directory. If not specified, it will be set to homedir() + * Files will be created under baseCachePath/.credo directory. If not specified, it will be set to homedir() * @param baseTempPath The base path to use for reading and writing temporary files within the framework. - * Files will be created under baseTempPath/.afj directory. If not specified, it will be set to tmpdir() + * Files will be created under baseTempPath/.credo directory. If not specified, it will be set to tmpdir() */ public constructor(options?: { baseDataPath?: string; baseCachePath?: string; baseTempPath?: string }) { - this.dataPath = options?.baseDataPath ? `${options?.baseDataPath}/.afj` : `${homedir()}/.afj/data` - this.cachePath = options?.baseCachePath ? `${options?.baseCachePath}/.afj` : `${homedir()}/.afj/cache` - this.tempPath = `${options?.baseTempPath ?? tmpdir()}/.afj` + this.dataPath = options?.baseDataPath ? `${options?.baseDataPath}/.credo` : `${homedir()}/.credo/data` + this.cachePath = options?.baseCachePath ? `${options?.baseCachePath}/.credo` : `${homedir()}/.credo/cache` + this.tempPath = `${options?.baseTempPath ?? tmpdir()}/.credo` + } + + /** + * Migrate data from .afj to .credo if .afj exists. + * Copy the contents from old directory (.afj) to new directory (.credo). + */ + public async migrateWalletToCredoFolder(walletId: string) { + try { + // We only migrate the specific wallet folder because other wallets might be using the same .afj folder + // which are used by different agents + const oldWalletPath = this.dataPath.replace('.credo/data', `.afj/data/wallet/${walletId}`) + const cacheAfjPath = this.cachePath.replace('.credo', '.afj') + const tempAfjPath = this.tempPath.replace('.credo', '.afj') + + const pathsToMigrate = [ + { + from: oldWalletPath, + // We manually construct the path to the wallet folder because we only want to migrate the specific wallet + to: this.dataPath + '/wallet/' + walletId, + }, + { + from: cacheAfjPath, + to: this.cachePath, + }, + { + from: tempAfjPath, + to: this.tempPath, + }, + ] + + for await (const path of pathsToMigrate) { + // Migrate if the old paths exist + if (await this.exists(path.from)) { + await this.copyDirectory(path.from, path.to) + } + } + } catch (error) { + throw new CredoError(`Error during migration from .afj to .credo`, { + cause: error, + }) + } + } + + public async copyDirectory(sourcePath: string, destinationPath: string) { + await cp(sourcePath, destinationPath, { recursive: true }) } public async exists(path: string) { @@ -49,7 +94,6 @@ export class NodeFileSystem implements FileSystem { } public async write(path: string, data: string): Promise { - // Make sure parent directories exist await mkdir(dirname(path), { recursive: true }) return writeFile(path, data, { encoding: 'utf-8' }) diff --git a/packages/node/tests/NodeFileSystem.test.ts b/packages/node/tests/NodeFileSystem.test.ts index c529b9d3df..c663701e5f 100644 --- a/packages/node/tests/NodeFileSystem.test.ts +++ b/packages/node/tests/NodeFileSystem.test.ts @@ -1,5 +1,6 @@ import { TypedArrayEncoder } from '@credo-ts/core' import nock, { cleanAll, enableNetConnect } from 'nock' +import { homedir } from 'os' import path from 'path' import { NodeFileSystem } from '../src/NodeFileSystem' @@ -38,5 +39,17 @@ describe('@credo-ts/file-system-node', () => { ) }) }) + + describe('migrateWalletToCredoFolder', () => { + test('should copy the files from .afj to .credo', async () => { + const filePath = `${homedir()}/.afj/data/wallet/test/testwallet.db` + + await fileSystem.write(filePath, 'some-random-content') + + await fileSystem.migrateWalletToCredoFolder('test') + + expect(await fileSystem.exists(`${fileSystem.dataPath}/wallet/test/testwallet.db`)).toBe(true) + }) + }) }) }) diff --git a/packages/react-native/src/ReactNativeFileSystem.ts b/packages/react-native/src/ReactNativeFileSystem.ts index 6711400698..33d6115db1 100644 --- a/packages/react-native/src/ReactNativeFileSystem.ts +++ b/packages/react-native/src/ReactNativeFileSystem.ts @@ -13,26 +13,88 @@ export class ReactNativeFileSystem implements FileSystem { * Create new ReactNativeFileSystem class instance. * * @param baseDataPath The base path to use for reading and writing data files used within the framework. - * Files will be created under baseDataPath/.afj directory. If not specified, it will be set to + * Files will be created under baseDataPath/.credo directory. If not specified, it will be set to * RNFS.DocumentDirectoryPath * @param baseCachePath The base path to use for reading and writing cache files used within the framework. - * Files will be created under baseCachePath/.afj directory. If not specified, it will be set to + * Files will be created under baseCachePath/.credo directory. If not specified, it will be set to * RNFS.CachesDirectoryPath * @param baseTempPath The base path to use for reading and writing temporary files within the framework. - * Files will be created under baseTempPath/.afj directory. If not specified, it will be set to + * Files will be created under baseTempPath/.credo directory. If not specified, it will be set to * RNFS.TemporaryDirectoryPath * * @see https://github.com/itinance/react-native-fs#constants */ public constructor(options?: { baseDataPath?: string; baseCachePath?: string; baseTempPath?: string }) { - this.dataPath = `${options?.baseDataPath ?? RNFS.DocumentDirectoryPath}/.afj` + this.dataPath = `${options?.baseDataPath ?? RNFS.DocumentDirectoryPath}/.credo` // In Android, TemporaryDirectoryPath falls back to CachesDirectoryPath this.cachePath = options?.baseCachePath - ? `${options?.baseCachePath}/.afj` - : `${RNFS.CachesDirectoryPath}/.afj${Platform.OS === 'android' ? '/cache' : ''}` + ? `${options?.baseCachePath}/.credo` + : `${RNFS.CachesDirectoryPath}/.credo${Platform.OS === 'android' ? '/cache' : ''}` this.tempPath = options?.baseTempPath - ? `${options?.baseTempPath}/.afj` - : `${RNFS.TemporaryDirectoryPath}/.afj${Platform.OS === 'android' ? '/temp' : ''}` + ? `${options?.baseTempPath}/.credo` + : `${RNFS.TemporaryDirectoryPath}/.credo${Platform.OS === 'android' ? '/temp' : ''}` + } + + /** + * Migrate data from .afj to .credo if .afj exists. + * Copy the contents from old directory (.afj) to new directory (.credo). + */ + public async migrateWalletToCredoFolder() { + try { + const oldDataPath = this.dataPath.replace('.credo', `.afj`) + const cacheAfjPath = this.cachePath.replace('.credo', '.afj') + const tempAfjPath = this.tempPath.replace('.credo', '.afj') + + const pathsToMigrate = [ + { + from: oldDataPath, + to: this.dataPath, + }, + { + from: cacheAfjPath, + to: this.cachePath, + }, + { + from: tempAfjPath, + to: this.tempPath, + }, + ] + + for await (const path of pathsToMigrate) { + // Migrate if the old paths exist + if (await this.exists(path.from)) { + await this.copyDirectory(path.from, path.to) + } + } + } catch (error) { + throw new CredoError(`Error during migration from .afj to .credo`, { + cause: error, + }) + } + } + + public async copyDirectory(sourcePath: string, destinationPath: string) { + try { + // Ensure the target directory exists + await RNFS.mkdir(destinationPath) + + // Get the contents of the source directory + const contents = await RNFS.readDir(sourcePath) + + for (const item of contents) { + const newPath = `${destinationPath}/${item.name}` + + if (item.isDirectory()) { + // Recursively copy subdirectories + await this.copyDirectory(item.path, newPath) + } else { + // Copy files to the new location + await RNFS.copyFile(item.path, newPath) + } + } + } catch (error) { + throw new CredoError(`Error copying directory from ${sourcePath} to ${destinationPath}`, { cause: error }) + } } public async exists(path: string): Promise {