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 6 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 `readonly`/`writeonly` 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, {readonly: true})
.use(fs2, {writeonly: true});

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 @@ -28,9 +28,6 @@
"test-coverage": "jest --coverage",
"semantic-release": "semantic-release"
},
"dependencies": {
"fs-monkey": "^1.0.0"
elmpp marked this conversation as resolved.
Show resolved Hide resolved
},
"devDependencies": {
"semantic-release": "15.14.0",
"@semantic-release/changelog": "3.0.6",
Expand Down
159 changes: 159 additions & 0 deletions src/__tests__/union.test.ts
Expand Up @@ -78,6 +78,53 @@ describe('union', () => {
});
});

describe("readonly/writeonly", () => {
it('writes to the last vol added', () => {
const vol1 = Volume.fromJSON({});
const vol2 = Volume.fromJSON({});
const ufs = new Union() as any;
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 non-readonly vol', () => {
const vol1 = Volume.fromJSON({});
const vol2 = Volume.fromJSON({});
const ufs = new Union() as any;
ufs.use(vol1 as any).use(vol2 as any, {readonly: true})
ufs.writeFileSync('/foo', 'bar')
expect(vol1.readFileSync('/foo', 'utf8')).toEqual('bar');
})

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

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

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

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

expect(ufs.readFileSync('/foo')).toBeUndefined()
})
});

describe("readdirSync", () => {
it('reads one memfs correctly', () => {
const vol = Volume.fromJSON({
Expand Down Expand Up @@ -232,6 +279,71 @@ describe('union', () => {

});

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

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

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

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

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

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

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

describe("readonly/writeonly", () => {
it('writes to the last vol added', () => {
const vol1 = Volume.fromJSON({});
const vol2 = Volume.fromJSON({});
const ufs = new Union() as any;
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 non-readonly vol', () => {
const vol1 = Volume.fromJSON({});
const vol2 = Volume.fromJSON({});
const ufs = new Union() as any;
ufs.use(vol1 as any).use(vol2 as any, {readonly: true})
ufs.writeFileSync('/foo', 'bar')
expect(vol1.readFileSync('/foo', 'utf8')).toEqual('bar');
})

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

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

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

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

expect(ufs.readFileSync('/foo')).toBeUndefined()
})
});

describe("readdir", () => {
it('reads one memfs correctly', async () => {
const vol = Volume.fromJSON({
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Expand Up @@ -2,7 +2,7 @@ import {Union as _Union} from "./union";
import {IFS} from "./fs";

export interface IUnionFs extends IFS {
use(fs: IFS): this;
use: (...args: Parameters<_Union['use']>) => 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 fsSyncMethodsWriteonly = [
"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 fsSyncMethodsReadonly = [
"accessSync",
"createReadStream",
"existsSync",
"fstatSync",
"ftruncateSync",
"openSync",
"readdirSync",
"readFileSync",
"readlinkSync",
"readSync",
"realpathSync",
"statSync"
] as const;
export const fsAsyncMethodsReadonly = [
"access",
"exists",
"fstat",
"open",
"read",
"readdir",
"readFile",
"readlink",
"realpath",
"unwatchFile",
"watch",
"watchFile"
] as const;
export const fsAsyncMethodsWriteonly = [
"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 fsPromiseMethodsReadonly = [
"access",
"open",
"opendir",
"readdir",
"readFile",
"readlink",
"realpath"
] as const;

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