Skip to content

Commit

Permalink
feat: 🎸 implement async verions of snapshotting
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich committed Jun 25, 2023
1 parent 4296509 commit 18912bf
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 57 deletions.
65 changes: 65 additions & 0 deletions src/snapshot/__tests__/async.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { toSnapshotSync, fromSnapshotSync } from '..';
import { toSnapshotSync, fromSnapshotSync } from '../sync';
import { memfs } from '../..';
import { SnapshotNodeType } from '../constants';

Expand Down
55 changes: 55 additions & 0 deletions src/snapshot/async.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { SnapshotNodeType } from './constants';
import type { AsyncSnapshotOptions, SnapshotNode } from './types';

export const toSnapshot = async ({ fs, path = '/', separator = '/' }: AsyncSnapshotOptions): Promise<SnapshotNode> => {
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<void> => {
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;
}
}
};
59 changes: 4 additions & 55 deletions src/snapshot/index.ts
Original file line number Diff line number Diff line change
@@ -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';
55 changes: 55 additions & 0 deletions src/snapshot/sync.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
};
8 changes: 7 additions & 1 deletion src/snapshot/type.ts → src/snapshot/types.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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 = [
Expand Down

0 comments on commit 18912bf

Please sign in to comment.