From 18912bfe9fd1f08bf0e2103b6d041a9a40937505 Mon Sep 17 00:00:00 2001 From: streamich Date: Sun, 25 Jun 2023 13:44:42 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20implement=20async=20veri?= =?UTF-8?q?ons=20of=20snapshotting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/snapshot/__tests__/async.test.ts | 65 +++++++++++++++++++ .../__tests__/{index.test.ts => sync.test.ts} | 2 +- src/snapshot/async.ts | 55 ++++++++++++++++ src/snapshot/index.ts | 59 ++--------------- src/snapshot/sync.ts | 55 ++++++++++++++++ src/snapshot/{type.ts => types.ts} | 8 ++- 6 files changed, 187 insertions(+), 57 deletions(-) create mode 100644 src/snapshot/__tests__/async.test.ts rename src/snapshot/__tests__/{index.test.ts => sync.test.ts} (97%) create mode 100644 src/snapshot/async.ts create mode 100644 src/snapshot/sync.ts rename src/snapshot/{type.ts => types.ts} (79%) diff --git a/src/snapshot/__tests__/async.test.ts b/src/snapshot/__tests__/async.test.ts new file mode 100644 index 000000000..1673970c8 --- /dev/null +++ b/src/snapshot/__tests__/async.test.ts @@ -0,0 +1,65 @@ +import { fromSnapshot, toSnapshot } from '../async'; +import { memfs } from '../..'; +import { SnapshotNodeType } from '../constants'; + +test('can snapshot a single file', async () => { + const { fs } = memfs({ + '/foo': 'bar', + }); + const snapshot = await toSnapshot({ fs: fs.promises, path: '/foo' }); + expect(snapshot).toStrictEqual([SnapshotNodeType.File, expect.any(Object), new Uint8Array([98, 97, 114])]); +}); + +test('can snapshot a single folder', async () => { + const { fs } = memfs({ + '/foo': null, + }); + const snapshot = await toSnapshot({ fs: fs.promises, path: '/foo' }); + expect(snapshot).toStrictEqual([SnapshotNodeType.Folder, expect.any(Object), {}]); +}); + +test('can snapshot a folder with a file and symlink', async () => { + const { fs } = memfs({ + '/foo': 'bar', + }); + fs.symlinkSync('/foo', '/baz'); + const snapshot = await toSnapshot({ fs: fs.promises, path: '/' }); + expect(snapshot).toStrictEqual([ + SnapshotNodeType.Folder, + expect.any(Object), + { + foo: [SnapshotNodeType.File, expect.any(Object), new Uint8Array([98, 97, 114])], + baz: [SnapshotNodeType.Symlink, { target: '/foo' }], + }, + ]); +}); + +test('can create a snapshot and un-snapshot a complex fs tree', async () => { + const { fs } = memfs({ + '/start': { + file1: 'file1', + file2: 'file2', + 'empty-folder': null, + '/folder1': { + file3: 'file3', + file4: 'file4', + 'empty-folder': null, + '/folder2': { + file5: 'file5', + file6: 'file6', + 'empty-folder': null, + 'empty-folde2': null, + }, + }, + }, + }); + fs.symlinkSync('/start/folder1/folder2/file6', '/start/folder1/symlink'); + fs.writeFileSync('/start/binary', new Uint8Array([1, 2, 3])); + const snapshot = await toSnapshot({ fs: fs.promises, path: '/start' })!; + const { fs: fs2, vol: vol2 } = memfs(); + fs2.mkdirSync('/start', { recursive: true }); + await fromSnapshot(snapshot, { fs: fs2.promises, path: '/start' }); + expect(fs2.readFileSync('/start/binary')).toStrictEqual(Buffer.from([1, 2, 3])); + const snapshot2 = await toSnapshot({ fs: fs2.promises, path: '/start' })!; + expect(snapshot2).toStrictEqual(snapshot); +}); diff --git a/src/snapshot/__tests__/index.test.ts b/src/snapshot/__tests__/sync.test.ts similarity index 97% rename from src/snapshot/__tests__/index.test.ts rename to src/snapshot/__tests__/sync.test.ts index fa5c139e3..4d1995851 100644 --- a/src/snapshot/__tests__/index.test.ts +++ b/src/snapshot/__tests__/sync.test.ts @@ -1,4 +1,4 @@ -import { toSnapshotSync, fromSnapshotSync } from '..'; +import { toSnapshotSync, fromSnapshotSync } from '../sync'; import { memfs } from '../..'; import { SnapshotNodeType } from '../constants'; diff --git a/src/snapshot/async.ts b/src/snapshot/async.ts new file mode 100644 index 000000000..70e7db0a0 --- /dev/null +++ b/src/snapshot/async.ts @@ -0,0 +1,55 @@ +import { SnapshotNodeType } from './constants'; +import type { AsyncSnapshotOptions, SnapshotNode } from './types'; + +export const toSnapshot = async ({ fs, path = '/', separator = '/' }: AsyncSnapshotOptions): Promise => { + const stats = await fs.lstat(path); + if (stats.isDirectory()) { + const list = await fs.readdir(path); + const entries: { [child: string]: SnapshotNode } = {}; + const dir = path.endsWith(separator) ? path : path + separator; + for (const child of list) { + const childSnapshot = await toSnapshot({ fs, path: `${dir}${child}`, separator }); + if (childSnapshot) entries['' + child] = childSnapshot; + } + return [SnapshotNodeType.Folder, {}, entries]; + } else if (stats.isFile()) { + const buf = await fs.readFile(path) as Buffer; + const uint8 = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); + return [SnapshotNodeType.File, {}, uint8]; + } else if (stats.isSymbolicLink()) { + return [ + SnapshotNodeType.Symlink, + { + target: await fs.readlink(path, {encoding: 'utf8'}) as string, + }, + ]; + } + return null; +}; + +export const fromSnapshot = async ( + snapshot: SnapshotNode, + { fs, path = '/', separator = '/' }: AsyncSnapshotOptions, +): Promise => { + if (!snapshot) return; + switch (snapshot[0]) { + case SnapshotNodeType.Folder: { + if (!path.endsWith(separator)) path = path + separator; + const [, , entries] = snapshot; + await fs.mkdir(path, { recursive: true }); + for (const [name, child] of Object.entries(entries)) + await fromSnapshot(child, { fs, path: `${path}${name}`, separator }); + break; + } + case SnapshotNodeType.File: { + const [, , data] = snapshot; + await fs.writeFile(path, data); + break; + } + case SnapshotNodeType.Symlink: { + const [, { target }] = snapshot; + await fs.symlink(target, path); + break; + } + } +}; diff --git a/src/snapshot/index.ts b/src/snapshot/index.ts index 502cc4578..66b91fb83 100644 --- a/src/snapshot/index.ts +++ b/src/snapshot/index.ts @@ -1,55 +1,4 @@ -import { SnapshotNodeType } from './constants'; -import type { SnapshotNode, SnapshotOptions } from './type'; - -export const toSnapshotSync = ({ fs, path = '/', separator = '/' }: SnapshotOptions): SnapshotNode => { - const stats = fs.lstatSync(path); - if (stats.isDirectory()) { - const list = fs.readdirSync(path); - const entries: { [child: string]: SnapshotNode } = {}; - const dir = path.endsWith(separator) ? path : path + separator; - for (const child of list) { - const childSnapshot = toSnapshotSync({ fs, path: `${dir}${child}`, separator }); - if (childSnapshot) entries['' + child] = childSnapshot; - } - return [SnapshotNodeType.Folder, {}, entries]; - } else if (stats.isFile()) { - const buf = fs.readFileSync(path) as Buffer; - const uint8 = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); - return [SnapshotNodeType.File, {}, uint8]; - } else if (stats.isSymbolicLink()) { - return [ - SnapshotNodeType.Symlink, - { - target: fs.readlinkSync(path).toString(), - }, - ]; - } - return null; -}; - -export const fromSnapshotSync = ( - snapshot: SnapshotNode, - { fs, path = '/', separator = '/' }: SnapshotOptions, -): void => { - if (!snapshot) return; - switch (snapshot[0]) { - case SnapshotNodeType.Folder: { - if (!path.endsWith(separator)) path = path + separator; - const [, , entries] = snapshot; - fs.mkdirSync(path, { recursive: true }); - for (const [name, child] of Object.entries(entries)) - fromSnapshotSync(child, { fs, path: `${path}${name}`, separator }); - break; - } - case SnapshotNodeType.File: { - const [, , data] = snapshot; - fs.writeFileSync(path, data); - break; - } - case SnapshotNodeType.Symlink: { - const [, { target }] = snapshot; - fs.symlinkSync(target, path); - break; - } - } -}; +export type * from './types'; +export * from './constants'; +export * from './sync'; +export * from './binary'; diff --git a/src/snapshot/sync.ts b/src/snapshot/sync.ts new file mode 100644 index 000000000..9f74471d4 --- /dev/null +++ b/src/snapshot/sync.ts @@ -0,0 +1,55 @@ +import { SnapshotNodeType } from './constants'; +import type { SnapshotNode, SnapshotOptions } from './types'; + +export const toSnapshotSync = ({ fs, path = '/', separator = '/' }: SnapshotOptions): SnapshotNode => { + const stats = fs.lstatSync(path); + if (stats.isDirectory()) { + const list = fs.readdirSync(path); + const entries: { [child: string]: SnapshotNode } = {}; + const dir = path.endsWith(separator) ? path : path + separator; + for (const child of list) { + const childSnapshot = toSnapshotSync({ fs, path: `${dir}${child}`, separator }); + if (childSnapshot) entries['' + child] = childSnapshot; + } + return [SnapshotNodeType.Folder, {}, entries]; + } else if (stats.isFile()) { + const buf = fs.readFileSync(path) as Buffer; + const uint8 = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); + return [SnapshotNodeType.File, {}, uint8]; + } else if (stats.isSymbolicLink()) { + return [ + SnapshotNodeType.Symlink, + { + target: fs.readlinkSync(path).toString(), + }, + ]; + } + return null; +}; + +export const fromSnapshotSync = ( + snapshot: SnapshotNode, + { fs, path = '/', separator = '/' }: SnapshotOptions, +): void => { + if (!snapshot) return; + switch (snapshot[0]) { + case SnapshotNodeType.Folder: { + if (!path.endsWith(separator)) path = path + separator; + const [, , entries] = snapshot; + fs.mkdirSync(path, { recursive: true }); + for (const [name, child] of Object.entries(entries)) + fromSnapshotSync(child, { fs, path: `${path}${name}`, separator }); + break; + } + case SnapshotNodeType.File: { + const [, , data] = snapshot; + fs.writeFileSync(path, data); + break; + } + case SnapshotNodeType.Symlink: { + const [, { target }] = snapshot; + fs.symlinkSync(target, path); + break; + } + } +}; diff --git a/src/snapshot/type.ts b/src/snapshot/types.ts similarity index 79% rename from src/snapshot/type.ts rename to src/snapshot/types.ts index 8c5c5191e..97e9498d0 100644 --- a/src/snapshot/type.ts +++ b/src/snapshot/types.ts @@ -1,4 +1,4 @@ -import type { FsSynchronousApi } from '../node/types'; +import type { FsPromisesApi, FsSynchronousApi } from '../node/types'; import type { SnapshotNodeType } from './constants'; export interface SnapshotOptions { @@ -7,6 +7,12 @@ export interface SnapshotOptions { separator?: '/' | '\\'; } +export interface AsyncSnapshotOptions { + fs: FsPromisesApi; + path?: string; + separator?: '/' | '\\'; +} + export type SnapshotNode = FolderNode | FileNode | SymlinkNode | UnknownNode; export type FolderNode = [