diff --git a/src/snapshot/__tests__/index.test.ts b/src/snapshot/__tests__/index.test.ts new file mode 100644 index 000000000..62ad60645 --- /dev/null +++ b/src/snapshot/__tests__/index.test.ts @@ -0,0 +1,73 @@ +import {toSnapshotSync, fromSnapshotSync} from '..'; +import {memfs} from '../..'; +import {SnapshotNodeType} from '../constants'; + +test('can snapshot a single file', () => { + const {fs} = memfs({ + '/foo': 'bar', + }); + const snapshot = toSnapshotSync({fs, path: '/foo'}); + expect(snapshot).toStrictEqual([ + SnapshotNodeType.File, + expect.any(Object), + new Uint8Array([98, 97, 114]), + ]); +}); + +test('can snapshot a single folder', () => { + const {fs} = memfs({ + '/foo': null, + }); + const snapshot = toSnapshotSync({fs, path: '/foo'}); + expect(snapshot).toStrictEqual([ + SnapshotNodeType.Folder, + expect.any(Object), + {}, + ]); +}); + +test('can snapshot a folder with a file and symlink', () => { + const {fs} = memfs({ + '/foo': 'bar', + }); + fs.symlinkSync('/foo', '/baz'); + const snapshot = toSnapshotSync({fs, 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', () => { + 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 = toSnapshotSync({fs, path: '/start'})!; + const {fs: fs2, vol: vol2} = memfs(); + fs2.mkdirSync('/start', {recursive: true}); + fromSnapshotSync(snapshot, {fs: fs2, path: '/start'}); + expect(fs2.readFileSync('/start/binary')).toStrictEqual(Buffer.from([1, 2, 3])); + const snapshot2 = toSnapshotSync({fs: fs2, path: '/start'})!; + expect(snapshot2).toStrictEqual(snapshot); +}); diff --git a/src/snapshot/constants.ts b/src/snapshot/constants.ts new file mode 100644 index 000000000..fa65dbf79 --- /dev/null +++ b/src/snapshot/constants.ts @@ -0,0 +1,5 @@ +export const enum SnapshotNodeType { + Folder = 0, + File = 1, + Symlink = 2, +} diff --git a/src/snapshot/index.ts b/src/snapshot/index.ts new file mode 100644 index 000000000..0badfc956 --- /dev/null +++ b/src/snapshot/index.ts @@ -0,0 +1,48 @@ +import {SnapshotNodeType} from "./constants"; +import type {SnapshotNode, SnapshotOptions} from "./type"; + +export const toSnapshotSync = ({fs, path = '/', separator = '/'}: SnapshotOptions): SnapshotNode | null => { + 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 => { + 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/type.ts new file mode 100644 index 000000000..572d09336 --- /dev/null +++ b/src/snapshot/type.ts @@ -0,0 +1,38 @@ +import type {FsSynchronousApi} from "../node/types"; +import type {SnapshotNodeType} from "./constants"; + +export interface SnapshotOptions { + fs: FsSynchronousApi; + path?: string; + separator?: '/' | '\\'; +} + +export type SnapshotNode = + | FolderNode + | FileNode + | SymlinkNode; + +export type FolderNode = [ + type: SnapshotNodeType.Folder, + meta: FolderMetadata, + entries: {[child: string]: SnapshotNode}, +]; + +export interface FolderMetadata {} + +export type FileNode = [ + type: SnapshotNodeType.File, + meta: FileMetadata, + data: Uint8Array, +]; + +export interface FileMetadata {} + +export type SymlinkNode = [ + type: SnapshotNodeType.Symlink, + meta: SymlinkMetadata, +]; + +export interface SymlinkMetadata { + target: string, +} diff --git a/src/volume.ts b/src/volume.ts index e25be56a6..b8210327b 100644 --- a/src/volume.ts +++ b/src/volume.ts @@ -1150,7 +1150,6 @@ export class Volume implements FsCallbackApi { lstatSync(path: PathLike, options: { bigint: true; throwIfNoEntry: false }): Stats | undefined; lstatSync(path: PathLike, options?: opts.IStatOptions): Stats | undefined { const { throwIfNoEntry = true, bigint = false } = getStatOptions(options); - return this.lstatBase(pathToFilename(path), bigint as any, throwIfNoEntry as any); } @@ -1168,7 +1167,6 @@ export class Volume implements FsCallbackApi { private statBase(filename: string, bigint: false, throwIfNoEntry: false): Stats | undefined; private statBase(filename: string, bigint = false, throwIfNoEntry = true): Stats | undefined { const link = this.getResolvedLink(filenameToSteps(filename)); - if (link) { return Stats.build(link.getNode(), bigint); } else if (!throwIfNoEntry) {