Skip to content

Commit

Permalink
feat: 🎸 add snapshot creation utilities
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich committed Jun 25, 2023
1 parent 5c3054e commit 9fc8f13
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 2 deletions.
73 changes: 73 additions & 0 deletions src/snapshot/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
5 changes: 5 additions & 0 deletions src/snapshot/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const enum SnapshotNodeType {
Folder = 0,
File = 1,
Symlink = 2,
}
48 changes: 48 additions & 0 deletions src/snapshot/index.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
};
38 changes: 38 additions & 0 deletions src/snapshot/type.ts
Original file line number Diff line number Diff line change
@@ -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,
}
2 changes: 0 additions & 2 deletions src/volume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1150,7 +1150,6 @@ export class Volume implements FsCallbackApi {
lstatSync(path: PathLike, options: { bigint: true; throwIfNoEntry: false }): Stats<bigint> | 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);
}

Expand All @@ -1168,7 +1167,6 @@ export class Volume implements FsCallbackApi {
private statBase(filename: string, bigint: false, throwIfNoEntry: false): Stats<number> | 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) {
Expand Down

0 comments on commit 9fc8f13

Please sign in to comment.