Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add readonly/writeonly options for volumes #441

Closed
wants to merge 26 commits into from
Closed
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -7,3 +7,4 @@ package-lock.json
/lib/
/demo/**/*.js
/src/**/*.js
*.test.ts.test
elmpp marked this conversation as resolved.
Show resolved Hide resolved
14 changes: 14 additions & 0 deletions README.md
Expand Up @@ -20,6 +20,20 @@ ufs
ufs.readFileSync(/* ... */);
```

This module allows you mark volumes as `readable`/`writable` (both defaulting to true) to prevent unwanted mutating of volumes

```js
import {ufs} from 'unionfs';
import {fs as fs1} from 'memfs';
import * as fs2 from 'fs';

ufs
.use(fs1, {writable: false})
.use(fs2, {readable: false});

ufs.writeFileSync(/* ... */); // fs2 will "collect" mutations; fs1 will remain unchanged
```

Use this module with [`memfs`][memfs] and [`linkfs`][linkfs].
`memfs` allows you to create virtual in-memory file system. `linkfs` allows you to redirect `fs` paths.

Expand Down
20 changes: 20 additions & 0 deletions demo/writeonly.ts
@@ -0,0 +1,20 @@
/**
* This example overlays a writeonly memfs volume which will effectively "collect" write operations
* and leave the underlying volume unchanged. This is useful for test suite scenarios
*
* Please also see [[../src/__tests__/union.test.ts]] for option examples
*/

import {ufs} from '../src';
import {Volume} from 'memfs';

const vol1 = Volume.fromJSON({
'/underlying_file': 'hello',
});
const vol2 = Volume.fromJSON({});

ufs.use(vol1 as any).use(vol2 as any, {writeonly: true})
elmpp marked this conversation as resolved.
Show resolved Hide resolved
ufs.writeFileSync('/foo', 'bar')

console.log(vol2.readFileSync('/foo', 'utf8')) // bar
console.log(vol2.readFileSync('/underlying_file', 'utf8')) // error
3 changes: 0 additions & 3 deletions package.json
Expand Up @@ -30,9 +30,6 @@
"prettier": "prettier --ignore-path .gitignore --write \"src/**/*.{ts,js}\"",
"prettier:diff": "prettier -l \"src/**/*.{ts,js}\""
},
"dependencies": {
"fs-monkey": "^1.0.0"
elmpp marked this conversation as resolved.
Show resolved Hide resolved
},
"devDependencies": {
"@semantic-release/changelog": "3.0.6",
"@semantic-release/git": "7.0.18",
Expand Down
158 changes: 158 additions & 0 deletions src/__tests__/union.test.ts
Expand Up @@ -76,6 +76,53 @@ describe('union', () => {
});
});

describe('readable/writable', () => {
it('writes to the last vol added', () => {
const vol1 = Volume.fromJSON({});
const vol2 = Volume.fromJSON({});
const ufs = new Union();
ufs.use(vol1 as any).use(vol2 as any);
ufs.writeFileSync('/foo', 'bar');
expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar');
});

it('writes to the latest added volume with writable=false vol', () => {
const vol1 = Volume.fromJSON({});
const vol2 = Volume.fromJSON({});
const ufs = new Union();
ufs.use(vol1 as any).use(vol2 as any, { writable: false });
ufs.writeFileSync('/foo', 'bar');
expect(vol1.readFileSync('/foo', 'utf8')).toEqual('bar');
});

it('writes to the latest added writable vol', () => {
const vol1 = Volume.fromJSON({});
const vol2 = Volume.fromJSON({});
const ufs = new Union();
ufs.use(vol1 as any).use(vol2 as any, { readable: false });
ufs.writeFileSync('/foo', 'bar');
expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar');
});

it('throw error if write operation attempted with all volumes writable=false', () => {
elmpp marked this conversation as resolved.
Show resolved Hide resolved
const vol1 = Volume.fromJSON({ '/foo': 'bar' });
const vol2 = Volume.fromJSON({ '/foo': 'bar' });
const ufs = new Union();
ufs.use(vol1 as any, { writable: false }).use(vol2 as any, { writable: false });

expect(() => ufs.writeFileSync('/foo', 'bar')).toThrowError();
});

it('throw error if read operation attempted with all volumes readable=false', () => {
elmpp marked this conversation as resolved.
Show resolved Hide resolved
const vol1 = Volume.fromJSON({ '/foo': 'bar1' });
const vol2 = Volume.fromJSON({ '/foo': 'bar2' });
const ufs = new Union();
ufs.use(vol1 as any, { readable: false }).use(vol2 as any, { readable: false });

expect(() => ufs.readFileSync('/foo')).toThrowError();
});
});

describe('readdirSync', () => {
it('reads one memfs correctly', () => {
const vol = Volume.fromJSON({
Expand Down Expand Up @@ -229,6 +276,70 @@ describe('union', () => {
});
});

describe('readable/writable', () => {
it('writes to the last vol added', done => {
const vol1 = Volume.fromJSON({});
const vol2 = Volume.fromJSON({});
const ufs = new Union();
ufs.use(vol1 as any).use(vol2 as any);
ufs.writeFile('/foo', 'bar', (err) => {
vol2.readFile('/foo', 'utf8', (err, res) => {
expect(res).toEqual('bar');
done();
});
});
});

it('writes to the latest added volume without writable=false', done => {
const vol1 = Volume.fromJSON({});
const vol2 = Volume.fromJSON({});
const ufs = new Union();
ufs.use(vol1 as any).use(vol2 as any, { writable: false });
ufs.writeFile('/foo', 'bar', (err) => {
vol1.readFile('/foo', 'utf8', (err, res) => {
expect(res).toEqual('bar');
done();
});
});
});

it('writes to the latest added writable vol', done => {
const vol1 = Volume.fromJSON({});
const vol2 = Volume.fromJSON({});
const ufs = new Union();
ufs.use(vol1 as any).use(vol2 as any, { readable: false });
ufs.writeFile('/foo', 'bar', (err) => {
vol2.readFile('/foo', 'utf8', (err, res) => {
expect(res).toEqual('bar');
done();
});
});
});

it('throw error if write operation attempted with all volumes writable=false', done => {
const vol1 = Volume.fromJSON({ '/foo': 'bar' });
const vol2 = Volume.fromJSON({ '/foo': 'bar' });
const ufs = new Union();
ufs.use(vol1 as any, { writable: false }).use(vol2 as any, { writable: false });
ufs.writeFile('/foo', 'bar', (err) => {
expect(err).toBeInstanceOf(Error);
done();
});
});

it('throw error if read operation attempted with all volumes readable=false', done => {
const vol1 = Volume.fromJSON({ '/foo': 'bar1' });
const vol2 = Volume.fromJSON({ '/foo': 'bar2' });
const ufs = new Union();
ufs.use(vol1 as any, { readable: false }).use(vol2 as any, { readable: false });

ufs.readFile('/foo', 'utf8', (err, res) => {
expect(err).toBeInstanceOf(Error);
done();
});
});
});

describe('readdir', () => {
it('reads one memfs correctly', () => {
const vol = Volume.fromJSON({
Expand Down Expand Up @@ -344,6 +455,53 @@ describe('union', () => {
await expect(ufs.promises.readFile('/foo', 'utf8')).rejects.toThrowError();
});

describe('readable/writable', () => {
it('writes to the last vol added', () => {
const vol1 = Volume.fromJSON({});
const vol2 = Volume.fromJSON({});
const ufs = new Union();
ufs.use(vol1 as any).use(vol2 as any);
ufs.writeFileSync('/foo', 'bar');
expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar');
});

it('writes to the latest added volume without writable=false', () => {
const vol1 = Volume.fromJSON({});
const vol2 = Volume.fromJSON({});
const ufs = new Union();
ufs.use(vol1 as any).use(vol2 as any, { writable: false });
ufs.writeFileSync('/foo', 'bar');
expect(vol1.readFileSync('/foo', 'utf8')).toEqual('bar');
});

it('writes to the latest added writable vol', () => {
const vol1 = Volume.fromJSON({});
const vol2 = Volume.fromJSON({});
const ufs = new Union();
ufs.use(vol1 as any).use(vol2 as any, { readable: false });
ufs.writeFileSync('/foo', 'bar');
expect(vol2.readFileSync('/foo', 'utf8')).toEqual('bar');
});

it('throw error if write operation attempted with all volumes writable=false', () => {
const vol1 = Volume.fromJSON({ '/foo': 'bar' });
const vol2 = Volume.fromJSON({ '/foo': 'bar' });
const ufs = new Union();
ufs.use(vol1 as any, { writable: false }).use(vol2 as any, { writable: false });

expect(() => ufs.writeFileSync('/foo', 'bar')).toThrowError();
});

it('throw error if read operation attempted with all volumes readable=false', () => {
const vol1 = Volume.fromJSON({ '/foo': 'bar1' });
const vol2 = Volume.fromJSON({ '/foo': 'bar2' });
const ufs = new Union();
ufs.use(vol1 as any, { readable: false }).use(vol2 as any, { readable: false });

expect(() => ufs.readFileSync('/foo')).toThrowError();
});
});

describe('readdir', () => {
it('reads one memfs correctly', async () => {
const vol = Volume.fromJSON({
Expand Down
5 changes: 3 additions & 2 deletions src/index.ts
@@ -1,8 +1,9 @@
import { Union as _Union } from './union';
import { Union as _Union, VolOptions } from './union';
import { IFS } from './fs';
export * from './lists'
elmpp marked this conversation as resolved.
Show resolved Hide resolved

export interface IUnionFs extends IFS {
use(fs: IFS): this;
use: (fs: IFS, options?: VolOptions) => this;
}

export const Union = (_Union as any) as new () => IUnionFs;
Expand Down
117 changes: 117 additions & 0 deletions src/lists.ts
@@ -0,0 +1,117 @@
export const fsSyncMethodsWrite = [
'appendFileSync',
'chmodSync',
'chownSync',
'closeSync',
'copyFileSync',
'createWriteStream',
'fchmodSync',
'fchownSync',
'fdatasyncSync',
'fsyncSync',
'futimesSync',
'lchmodSync',
'lchownSync',
'linkSync',
'lstatSync',
'mkdirpSync',
'mkdirSync',
'mkdtempSync',
'renameSync',
'rmdirSync',
'symlinkSync',
'truncateSync',
'unlinkSync',
'utimesSync',
'writeFileSync',
'writeSync',
] as const;

export const fsSyncMethodsRead = [
'accessSync',
'createReadStream',
'existsSync',
'fstatSync',
'ftruncateSync',
'openSync',
'readdirSync',
'readFileSync',
'readlinkSync',
'readSync',
'realpathSync',
'statSync',
] as const;
export const fsAsyncMethodsRead = [
'access',
'exists',
'fstat',
'open',
'read',
'readdir',
'readFile',
'readlink',
'realpath',
'unwatchFile',
'watch',
'watchFile',
] as const;
export const fsAsyncMethodsWrite = [
'appendFile',
'chmod',
'chown',
'close',
'copyFile',
'fchmod',
'fchown',
'fdatasync',
'fsync',
'ftruncate',
'futimes',
'lchmod',
'lchown',
'link',
'lstat',
'mkdir',
'mkdirp',
'mkdtemp',
'rename',
'rmdir',
'stat',
'symlink',
'truncate',
'unlink',
'utimes',
'write',
'writeFile',
] as const;

export const fsPromiseMethodsRead = [
'access',
'open',
'opendir',
'readdir',
'readFile',
'readlink',
'realpath',
] as const;

export const fsPromiseMethodsWrite = [
'appendFile',
'chmod',
'chown',
'copyFile',
'lchmod',
'lchown',
'link',
'lstat',
'mkdir',
'mkdtemp',
'rename',
'rmdir',
'stat',
'symlink',
'truncate',
'unlink',
'utimes',
'writeFile',
] as const;