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

feat: implement lutimes #1065

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
465764e
test: Add tests for permissions
BadIdeaException Sep 9, 2024
d25e21a
feat: Add permissions implementations to Volume
BadIdeaException Sep 9, 2024
d942282
fix: Side effects test pollution in chmod() test
BadIdeaException Sep 9, 2024
6daebcd
fix: Improper handling of O_EXCL in openFile()
BadIdeaException Sep 9, 2024
4e96b81
fix: No longer check execute permission on files in mkdirp
BadIdeaException Sep 9, 2024
06ee201
test: Add tests for recursive mkdir
BadIdeaException Sep 13, 2024
68836a5
test: Add tests for chmodSync
BadIdeaException Sep 13, 2024
ed3e6d1
fix: No longer require permissions to chmod
BadIdeaException Sep 13, 2024
b5ac3fb
test: Add more extensive tests for mkdir recursive mode
BadIdeaException Sep 14, 2024
137ef35
refactor: Inline tree walking
BadIdeaException Sep 14, 2024
1030618
chore: cleanup
BadIdeaException Sep 14, 2024
9422c7b
style: Run prettier
BadIdeaException Sep 14, 2024
e6b8229
test: Improve testing for missing permissions on intermediate directo…
BadIdeaException Sep 15, 2024
f80037f
chore: cleanup
BadIdeaException Sep 15, 2024
63172a8
style: run prettier
BadIdeaException Sep 16, 2024
5668c73
fix: utimes should not require permissions
BadIdeaException Sep 17, 2024
1713bd5
docs: `dir` was renamed to `path` a while back (#1060)
joscha Sep 17, 2024
1b08f3b
fix: add `parentPath` to `Dirent` (#1058)
SoulKa Sep 17, 2024
29d2fa0
chore(release): 4.11.2 [skip ci]
semantic-release-bot Sep 17, 2024
b4ef900
chore(deps): bump express from 4.19.2 to 4.21.0 (#1061)
dependabot[bot] Sep 17, 2024
48ebc96
chore(release): 4.12.0 [skip ci]
semantic-release-bot Sep 19, 2024
dd3f74d
feat: implement lutimes
BadIdeaException Oct 1, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
# [4.12.0](https://github.com/streamich/memfs/compare/v4.11.2...v4.12.0) (2024-09-19)


### Features

* improve permissions support ([#1059](https://github.com/streamich/memfs/issues/1059)) ([575a76b](https://github.com/streamich/memfs/commit/575a76b5f538f65ca5b8813073bc783f3d408a1b))

## [4.11.2](https://github.com/streamich/memfs/compare/v4.11.1...v4.11.2) (2024-09-17)


### Bug Fixes

* add `parentPath` to `Dirent` ([#1058](https://github.com/streamich/memfs/issues/1058)) ([9156c84](https://github.com/streamich/memfs/commit/9156c8466b530ad985b462890c7164dfeeaf472f)), closes [#735](https://github.com/streamich/memfs/issues/735) [#735](https://github.com/streamich/memfs/issues/735)

## [4.11.1](https://github.com/streamich/memfs/compare/v4.11.0...v4.11.1) (2024-08-01)


Expand Down
24 changes: 12 additions & 12 deletions docs/snapshot/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,15 @@ import * as snapshot from 'memfs/lib/snapshot';
You can convert any folder of an `fs`-like file system into a POJO snapshot.

```ts
const snap = snapshot.toSnapshotSync({ fs, dir });
const snap = await snapshot.toSnapshot({ fs: fs.promises, dir });
const snap = snapshot.toSnapshotSync({ fs, path });
const snap = await snapshot.toSnapshot({ fs: fs.promises, path });
```

Then import it back from snapshot.

```ts
snapshot.fromSnapshotSync(snap, { fs, dir });
await snapshot.fromSnapshot(snap, { fs: fs.promises, dir });
snapshot.fromSnapshotSync(snap, { fs, path });
await snapshot.fromSnapshot(snap, { fs: fs.promises, path });
```

## Binary snapshot
Expand All @@ -47,15 +47,15 @@ Binary snapshots are encoded as CBOR `Uint8Array` buffers. You can convert any
folder of an `fs`-like file system into a `Uint8Array` snapshot.

```ts
const uint8 = snapshot.toBinarySnapshotSync({ fs, dir });
const uint8 = await snapshot.toBinarySnapshot({ fs: fs.promises, dir });
const uint8 = snapshot.toBinarySnapshotSync({ fs, path });
const uint8 = await snapshot.toBinarySnapshot({ fs: fs.promises, path });
```

Then import it back from `Uint8Array` snapshot.

```ts
snapshot.fromBinarySnapshotSync(uint8, { fs, dir });
await snapshot.fromBinarySnapshot(uint8, { fs: fs.promises, dir });
snapshot.fromBinarySnapshotSync(uint8, { fs, path });
await snapshot.fromBinarySnapshot(uint8, { fs: fs.promises, path });
```

## JSON snapshot
Expand All @@ -67,15 +67,15 @@ data is encoded as Base64 data URL strings. The resulting JSON is returned as
You can convert any folder of an `fs`-like file system into a `Uint8Array` snapshot.

```ts
const uint8 = snapshot.toJsonSnapshotSync({ fs, dir });
const uint8 = await snapshot.toJsonSnapshot({ fs: fs.promises, dir });
const uint8 = snapshot.toJsonSnapshotSync({ fs, path });
const uint8 = await snapshot.toJsonSnapshot({ fs: fs.promises, path });
```

Then import it back from `Uint8Array` snapshot.

```ts
snapshot.fromJsonSnapshotSync(uint8, { fs, dir });
await snapshot.fromJsonSnapshot(uint8, { fs: fs.promises, dir });
snapshot.fromJsonSnapshotSync(uint8, { fs, path });
await snapshot.fromJsonSnapshot(uint8, { fs: fs.promises, path });
```

## Encoding format
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "memfs",
"version": "4.11.1",
"version": "4.12.0",
"description": "In-memory file-system with Node's fs API.",
"keywords": [
"fs",
Expand Down
2 changes: 2 additions & 0 deletions src/Dirent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ export class Dirent implements IDirent {
dirent.name = strToEncoding(link.getName(), encoding);
dirent.mode = mode;
dirent.path = link.getParentPath();
dirent.parentPath = dirent.path;

return dirent;
}

name: TDataOut = '';
path = '';
parentPath = '';
private mode: number = 0;

private _checkModeProperty(property: number): boolean {
Expand Down
12 changes: 8 additions & 4 deletions src/__tests__/promises.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,22 @@ describe('Promises API', () => {
});
});
describe('chmod(mode)', () => {
const vol = new Volume();
const { promises } = vol;
vol.fromJSON({
'/foo': 'bar',
let vol;
beforeEach(() => {
vol = new Volume();
vol.fromJSON({
'/foo': 'bar',
});
});
it('Change mode of existing file', async () => {
const { promises } = vol;
const fileHandle = await promises.open('/foo', 'a');
await fileHandle.chmod(0o444);
expect(vol.statSync('/foo').mode & 0o777).toEqual(0o444);
await fileHandle.close();
});
it('Reject when the file handle was closed', async () => {
const { promises } = vol;
const fileHandle = await promises.open('/foo', 'a');
await fileHandle.close();
return expect(fileHandle.chmod(0o666)).rejects.toBeInstanceOf(Error);
Expand Down
12 changes: 12 additions & 0 deletions src/__tests__/util.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import { createFsFromVolume, Volume } from '..';
import { Link, Node } from '../node';

// Turn the done callback into an incremental one that will only fire after being called
// `times` times, failing with the first reported error if such exists.
// Useful for testing callback-style functions with several different fixtures without
// having to clutter the test suite with a multitude of individual tests (like it.each would).
export const multitest = (_done: (err?: Error) => void, times: number) => {
let err;
return function done(_err?: Error) {
err ??= _err;
if (!--times) _done(_err);
};
};

export const create = (json: { [s: string]: string } = { '/foo': 'bar' }) => {
const vol = Volume.fromJSON(json);
return vol;
Expand Down
42 changes: 42 additions & 0 deletions src/__tests__/volume/ReadStream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,46 @@ describe('ReadStream', () => {
done();
});
});

it('should emit EACCES error when file has insufficient permissions', done => {
const fs = createFs({ '/test': 'test' });
fs.chmodSync('/test', 0o333); // wx
new fs.ReadStream('/test')
.on('error', err => {
expect(err).toBeInstanceOf(Error);
expect(err).toHaveProperty('code', 'EACCES');
done();
})
.on('open', () => {
done(new Error("Expected ReadStream to emit EACCES but it didn't"));
});
});

it('should emit EACCES error when containing directory has insufficient permissions', done => {
const fs = createFs({ '/foo/test': 'test' });
fs.chmodSync('/foo', 0o666); // rw
new fs.ReadStream('/foo/test')
.on('error', err => {
expect(err).toBeInstanceOf(Error);
expect(err).toHaveProperty('code', 'EACCES');
done();
})
.on('open', () => {
done(new Error("Expected ReadStream to emit EACCES but it didn't"));
});
});

it('should emit EACCES error when intermediate directory has insufficient permissions', done => {
const fs = createFs({ '/foo/test': 'test' });
fs.chmodSync('/', 0o666); // rw
new fs.ReadStream('/foo/test')
.on('error', err => {
expect(err).toBeInstanceOf(Error);
expect(err).toHaveProperty('code', 'EACCES');
done();
})
.on('open', () => {
done(new Error("Expected ReadStream to emit EACCES but it didn't"));
});
});
});
56 changes: 56 additions & 0 deletions src/__tests__/volume/WriteStream.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,60 @@ describe('WriteStream', () => {
done();
});
});

it('should emit EACCES error when file has insufficient permissions', done => {
const fs = createFs({ '/test': 'test' });
fs.chmodSync('/test', 0o555); // rx
new fs.WriteStream('/test')
.on('error', err => {
expect(err).toBeInstanceOf(Error);
expect(err).toHaveProperty('code', 'EACCES');
done();
})
.on('open', () => {
done(new Error("Expected WriteStream to emit EACCES but it didn't"));
});
});

it('should emit EACCES error for an existing file when containing directory has insufficient permissions', done => {
const fs = createFs({ '/foo/test': 'test' });
fs.chmodSync('/foo', 0o666); // rw
new fs.WriteStream('/foo/test')
.on('error', err => {
expect(err).toBeInstanceOf(Error);
expect(err).toHaveProperty('code', 'EACCES');
done();
})
.on('open', () => {
done(new Error("Expected WriteStream to emit EACCES but it didn't"));
});
});

it('should emit EACCES error for when intermediate directory has insufficient permissions', done => {
const fs = createFs({ '/foo/test': 'test' });
fs.chmodSync('/', 0o666); // rw
new fs.WriteStream('/foo/test')
.on('error', err => {
expect(err).toBeInstanceOf(Error);
expect(err).toHaveProperty('code', 'EACCES');
done();
})
.on('open', () => {
done(new Error("Expected WriteStream to emit EACCES but it didn't"));
});
});

it('should emit EACCES error for a non-existent file when containing directory has insufficient permissions', done => {
const fs = createFs({});
fs.mkdirSync('/foo', { mode: 0o555 }); // rx
new fs.WriteStream('/foo/test')
.on('error', err => {
expect(err).toBeInstanceOf(Error);
expect(err).toHaveProperty('code', 'EACCES');
done();
})
.on('open', () => {
done(new Error("Expected WriteStream to emit EACCES but it didn't"));
});
});
});
53 changes: 52 additions & 1 deletion src/__tests__/volume/appendFile.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { create } from '../util';
import { create, multitest } from '../util';

describe('appendFile(file, data[, options], callback)', () => {
it('Simple write to non-existing file', done => {
Expand All @@ -15,4 +15,55 @@ describe('appendFile(file, data[, options], callback)', () => {
done();
});
});

it('Appending gives EACCES without sufficient permissions on the file', done => {
const vol = create({ '/foo': 'foo' });
vol.chmodSync('/foo', 0o555); // rx across the board
vol.appendFile('/foo', 'bar', err => {
try {
expect(err).toBeInstanceOf(Error);
expect(err).toHaveProperty('code', 'EACCES');
done();
} catch (failure) {
done(failure);
}
});
});

it('Appending gives EACCES if file does not exist and containing directory has insufficient permissions', _done => {
const perms = [
0o555, // rx across the board
0o666, // rw across the board
];
const done = multitest(_done, perms.length);

perms.forEach(perm => {
const vol = create({});
vol.mkdirSync('/foo', { mode: perm });
vol.appendFile('/foo/test', 'bar', err => {
try {
expect(err).toBeInstanceOf(Error);
expect(err).toHaveProperty('code', 'EACCES');
done();
} catch (failure) {
done(failure);
}
});
});
});

it('Appending gives EACCES if intermediate directory has insufficient permissions', done => {
const vol = create({});
vol.mkdirSync('/foo');
vol.chmodSync('/', 0o666); // rw
vol.appendFile('/foo/test', 'bar', err => {
try {
expect(err).toBeInstanceOf(Error);
expect(err).toHaveProperty('code', 'EACCES');
done();
} catch (failure) {
done(failure);
}
});
});
});
29 changes: 29 additions & 0 deletions src/__tests__/volume/appendFileSync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,33 @@ describe('appendFileSync(file, data, options)', () => {
vol.appendFileSync('/a', 'c');
expect(vol.readFileSync('/a', 'utf8')).toEqual('bc');
});
it('Appending throws EACCES without sufficient permissions on the file', () => {
const vol = create({ '/foo': 'foo' });
vol.chmodSync('/foo', 0o555); // rx across the board
expect(() => {
vol.appendFileSync('/foo', 'bar');
}).toThrowError(/EACCES/);
});
it('Appending throws EACCES if file does not exist and containing directory has insufficient permissions', () => {
const perms = [
0o555, // rx across the board
// 0o666, // rw across the board
// 0o111, // x
// 0o222 // w
];
perms.forEach(perm => {
const vol = create({});
vol.mkdirSync('/foo', perm);
expect(() => {
vol.appendFileSync('/foo/test', 'bar');
}).toThrowError(/EACCES/);
});
});
it('Appending throws EACCES if intermediate directory has insufficient permissions', () => {
const vol = create({ '/foo/test': 'test' });
vol.chmodSync('/', 0o666); // rw
expect(() => {
vol.appendFileSync('/foo/test', 'bar');
}).toThrowError(/EACCES/);
});
});
Loading