Skip to content

Commit

Permalink
docs: ✏️ add demos
Browse files Browse the repository at this point in the history
  • Loading branch information
streamich committed Apr 30, 2024
1 parent 48d742d commit 558fd23
Show file tree
Hide file tree
Showing 26 changed files with 940 additions and 259 deletions.
15 changes: 15 additions & 0 deletions demo/crud-and-cas/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
This demo showcases `crudfs` and `casfs` interoperability in browser and Node.js.

First, in browser an object is stored into `casfs` (Content Addressable Storage)
and its hash (CID) is persisted using `crudfs`, in a real user folder.

Then, from Node.js, the CID is retrieved using `crudfs` and the object is read
using `casfs`.

https://github.com/streamich/memfs/assets/9773803/02ba339c-6e13-4712-a02f-672674300d27

Run:

```
yarn demo:git-fsa
```
38 changes: 38 additions & 0 deletions demo/crud-and-cas/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { FsaCrud } from '../../src/fsa-to-crud';
import { CrudCas } from '../../src/crud-to-cas';
import type * as fsa from '../../src/fsa/types';

const hash = async (data: Uint8Array): Promise<string> => {
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashHex;
};

const demo = async (dir: fsa.IFileSystemDirectoryHandle) => {
try {
const crud = new FsaCrud(dir);
const cas = new CrudCas(await crud.from(['objects']), { hash });

// Store "Hello, world!" in object storage
const cid = await cas.put(new TextEncoder().encode('Hello, world!'));

// Store the CID in the refs/heads/main.txt file
await crud.put(['refs', 'heads'], 'main.txt', new TextEncoder().encode(cid));
} catch (error) {
console.log(error);
console.log((<any>error).name);
}
};

const main = async () => {
const button = document.createElement('button');
button.textContent = 'Select an empty folder';
document.body.appendChild(button);
button.onclick = async () => {
const dir = await (window as any).showDirectoryPicker({ id: 'demo', mode: 'readwrite' });
await demo(dir);
};
};

main();
32 changes: 32 additions & 0 deletions demo/crud-and-cas/node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Run: npx ts-node demo/crud-and-cas/node.ts

import { NodeCrud } from '../../src/node-to-crud';
import { CrudCas } from '../../src/crud-to-cas';
import * as fs from 'fs';
const root = require('app-root-path');
const path = require('path');

const hash = async (data: Uint8Array): Promise<string> => {
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashHex;
};

const main = async () => {
const dir = path.resolve(root.path, 'fs-test');
const crud = new NodeCrud({ fs: <any>fs.promises, dir });
const cas = new CrudCas(await crud.from(['objects']), { hash });

// Retrieve the CID from the refs/heads/main.txt file
const cid = await crud.get(['refs', 'heads'], 'main.txt');
const cidText = Buffer.from(cid).toString();
console.log('CID:', cidText);

// Retrieve the data from the object storage
const data = await cas.get(cidText);
const dataText = Buffer.from(data).toString();
console.log('Content:', dataText);
};

main();
36 changes: 36 additions & 0 deletions demo/crud-and-cas/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const root = require('app-root-path');

module.exports = {
mode: 'development',
devtool: 'inline-source-map',
entry: {
bundle: __dirname + '/main',
},
plugins: [
new HtmlWebpackPlugin({
title: 'Development',
}),
],
module: {
rules: [
{
test: /\.tsx?$/,
exclude: /node_modules/,
loader: 'ts-loader',
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
output: {
filename: '[name].js',
path: path.resolve(root.path, 'dist'),
},
devServer: {
port: 9876,
hot: false,
},
};
19 changes: 19 additions & 0 deletions demo/fsa-to-node-sync-tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Synchronous `fs` in browser

This demo executes tests of **synchronous** Node.js API built on top of File
System Access API in browser.

There are two tests:

- OPFS &mdash; virtual file system built into browsers.
- Native File System Access API folder &mdash; a real folder that user explicitly grants permission to.

See the assertions run in the browser console.

https://github.com/streamich/memfs/assets/9773803/f841b6cc-728d-4341-b426-3daa6b43f8ac

Run:

```
demo:fsa-to-node-sync-tests
```
129 changes: 129 additions & 0 deletions demo/fsa-to-node-sync-tests/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
(window as any).process = require('process/browser');
(window as any).Buffer = require('buffer').Buffer;

import { strictEqual, deepEqual } from 'assert';

import type * as fsa from '../../src/fsa/types';
import { FsaNodeFs, FsaNodeSyncAdapterWorker } from '../../src/fsa-to-node';

const demo = async (dir: fsa.IFileSystemDirectoryHandle) => {
const adapter = await FsaNodeSyncAdapterWorker.start('https://localhost:9876/worker.js', dir);
const fs = new FsaNodeFs(dir, adapter);

await fs.promises.mkdir('/dir');
await fs.promises.writeFile('/test.txt', 'Hello world!');
const list = await fs.promises.readdir('');
console.log(list);

console.log('existsSync()');
strictEqual(fs.existsSync('/test.txt'), true);

console.log('statSync() - returns correct type for file');
strictEqual(fs.statSync('/test.txt').isFile(), true);
strictEqual(fs.statSync('/test.txt').isDirectory(), false);

console.log('statSync() - returns correct type for directory');
strictEqual(fs.statSync('/dir').isFile(), false);
strictEqual(fs.statSync('/dir').isDirectory(), true);

console.log('readFileSync() - can read file as text');
strictEqual(fs.readFileSync('/test.txt', 'utf8'), 'Hello world!');

console.log('writeFileSync() - can write text to a new file');
fs.writeFileSync('/cool.txt', 'worlds');
strictEqual(fs.readFileSync('/cool.txt', 'utf8'), 'worlds');

console.log('appendFileSync() - can append to an existing file');
fs.appendFileSync('/cool.txt', '!');
strictEqual(fs.readFileSync('/cool.txt', 'utf8'), 'worlds!');

console.log('copyFileSync() - can copy a file');
fs.copyFileSync('/cool.txt', '/cool (Copy).txt');
strictEqual(fs.readFileSync('/cool (Copy).txt', 'utf8'), 'worlds!');

console.log('renameSync() - can move a file');
fs.renameSync('/cool (Copy).txt', '/dir/very-cool.txt');
strictEqual(fs.readFileSync('/dir/very-cool.txt', 'utf8'), 'worlds!');

console.log('rmdirSync() - can remove an empty directory');
await fs.promises.mkdir('/to-be-deleted');
strictEqual(fs.existsSync('/to-be-deleted'), true);
fs.rmdirSync('/to-be-deleted');
strictEqual(fs.existsSync('/to-be-deleted'), false);

console.log('rmSync() - can delete a file');
await fs.promises.writeFile('/dir/tmp', '...');
strictEqual(fs.existsSync('/dir/tmp'), true);
fs.rmSync('/dir/tmp');
strictEqual(fs.existsSync('/dir/tmp'), false);

console.log('mkdirSync() - can create a nested directory');
fs.mkdirSync('/public/site/assets/img', { recursive: true });
strictEqual(fs.statSync('/public/site/assets/img').isDirectory(), true);

console.log('mkdtempSync() - can create a temporary directory');
await fs.promises.mkdir('/tmp');
const tmpDirName = fs.mkdtempSync('/tmp/temporary-');
strictEqual(fs.statSync(tmpDirName).isDirectory(), true);

console.log('truncateSync() - can truncate a file');
await fs.promises.writeFile('/truncated.txt', 'truncate here: abcdefghijklmnopqrstuvwxyz');
fs.truncateSync('/truncated.txt', 14);
strictEqual(fs.readFileSync('/truncated.txt', 'utf8'), 'truncate here:');

console.log('unlinkSync() - can delete a file');
await fs.promises.writeFile('/delete-me.txt', 'abc');
fs.unlinkSync('/delete-me.txt');
strictEqual(fs.existsSync('/delete-me.txt'), false);

console.log('readdirSync() - can list files in a directory');
const listInDir = fs.readdirSync('/dir');
deepEqual(listInDir, ['very-cool.txt']);

console.log('readdirSync() - can list files in a directory as Dirent[]');
const listInDir2 = fs.readdirSync('/dir', { withFileTypes: true }) as any;
deepEqual(listInDir2[0].name, 'very-cool.txt');
deepEqual(listInDir2[0].isFile(), true);

console.log('readSync() - can read a file into a buffer');
const buf = Buffer.alloc(3);
const readHandle = await fs.promises.open('/cool.txt', 'r');
const bytesRead = fs.readSync(readHandle.fd, buf, 0, 3, 0);
strictEqual(bytesRead, 3);
strictEqual(buf.toString('utf8'), 'wor');

console.log('writeSync() - can write into an open file');
const writeHandle = await fs.promises.open('/cool.txt', 'a');
const bytesWritten = fs.writeSync(writeHandle.fd, Buffer.from('W'), 0, 1, 0);
await writeHandle.close();
strictEqual(bytesWritten, 1);
strictEqual(fs.readFileSync('/cool.txt', 'utf8'), 'Worlds!');

console.log('openSync() - can create a file');
strictEqual(fs.existsSync('/new-file.txt'), false);
const fd = fs.openSync('/new-file.txt', 'w');
strictEqual(fs.existsSync('/new-file.txt'), true);
fs.unlinkSync('/new-file.txt');
strictEqual(typeof fd, 'number');
};

const main = async () => {
const button = document.createElement('button');
button.textContent = 'Select an empty folder';
document.body.appendChild(button);
button.onclick = async () => {
const dir = await (window as any).showDirectoryPicker({ id: 'demo', mode: 'readwrite' });
await demo(dir);
};

const button2 = document.createElement('button');
button2.textContent = 'Run tests in OPFS';
button2.style.marginLeft = '1em';
document.body.appendChild(button2);
button2.onclick = async () => {
const dir = await navigator.storage.getDirectory();
await demo(dir as any);
};
};

main();
56 changes: 56 additions & 0 deletions demo/fsa-to-node-sync-tests/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const root = require('app-root-path');

module.exports = {
mode: 'development',
devtool: 'inline-source-map',
entry: {
bundle: __dirname + '/main',
worker: __dirname + '/worker',
},
plugins: [
// new ForkTsCheckerWebpackPlugin(),
new HtmlWebpackPlugin({
title: 'Development',
}),
],
module: {
rules: [
{
test: /\.tsx?$/,
exclude: /node_modules/,
loader: 'ts-loader',
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
fallback: {
assert: require.resolve('assert'),
buffer: require.resolve('buffer'),
path: require.resolve('path-browserify'),
process: require.resolve('process/browser'),
// stream: require.resolve('streamx'),
stream: require.resolve('readable-stream'),
url: require.resolve('url'),
util: require.resolve('util'),
// fs: path.resolve(__dirname, '../../src/index.ts'),
},
},
output: {
filename: '[name].js',
path: path.resolve(root.path, 'dist'),
},
devServer: {
// HTTPS is required for SharedArrayBuffer to work.
https: true,
headers: {
// These two headers are required for SharedArrayBuffer to work.
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
},
port: 9876,
hot: false,
},
};
9 changes: 9 additions & 0 deletions demo/fsa-to-node-sync-tests/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
(self as any).process = require('process/browser');
(self as any).Buffer = require('buffer').Buffer;

import { FsaNodeSyncWorker } from '../../src/fsa-to-node/worker/FsaNodeSyncWorker';

if (typeof window === 'undefined') {
const worker = new FsaNodeSyncWorker();
worker.start();
}
17 changes: 17 additions & 0 deletions demo/fsa-to-node-zipfile/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
This demo shows how `tar-stream` package can be used to create a zip file from
a folder selected with File System Access API.

Below demo uses the File System Access API in the browser to get access to a real folder
on the file system. It then converts a [`FileSystemDirectoryHandle`](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryHandle) instance
to a Node-like `fs` file system.

It then creates two text files using `fs.promises.writeFile`, and then uses `tar-stream` package to create a zip file
and write it back got the file system using Node's `fs.createWriteStream`.

https://github.com/streamich/memfs/assets/9773803/8dc61d1e-61bf-4dfc-973b-028332fd4473

Run:

```
demo:fsa-to-node-zipfile
```
39 changes: 39 additions & 0 deletions demo/fsa-to-node-zipfile/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
(window as any).process = require('process/browser');
(window as any).Buffer = require('buffer').Buffer;

import type * as fsa from '../../src/fsa/types';
import { FsaNodeFs } from '../../src/fsa-to-node';
const tar = require('tar-stream');

const demo = async (dir: fsa.IFileSystemDirectoryHandle) => {
const fs = new FsaNodeFs(dir);
await fs.promises.writeFile('hello.txt', 'Hello World');
await fs.promises.writeFile('cool.txt', 'Cool Worlds channel');

const list = (await fs.promises.readdir('/')) as string[];

const pack = tar.pack();
const tarball = fs.createWriteStream('backups.tar');
pack.pipe(tarball);

for (const item of list) {
if (item[0] === '.') continue;
const stat = await fs.promises.stat(item);
if (!stat.isFile()) continue;
pack.entry({ name: '/backups/' + item }, await fs.promises.readFile('/' + item), () => {});
}

pack.finalize();
};

const main = async () => {
const button = document.createElement('button');
button.textContent = 'Select an empty folder';
document.body.appendChild(button);
button.onclick = async () => {
const dir = await (window as any).showDirectoryPicker({ id: 'demo', mode: 'readwrite' });
await demo(dir);
};
};

main();
Loading

0 comments on commit 558fd23

Please sign in to comment.