Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions .changeset/tiny-cats-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@effect/platform-node-shared": patch
"effect": patch
---

Add `FileSystem.lstat` for inspecting symbolic links without following them.
9 changes: 9 additions & 0 deletions packages/effect/src/FileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,12 @@ export interface FileSystem {
readonly mode?: number | undefined
}
) => Sink.Sink<void, Uint8Array, never, PlatformError>
/**
* Get information about a file at `path` without following symbolic links.
*/
readonly lstat: (
path: string
) => Effect.Effect<File.Info, PlatformError>
/**
* Get information about a file at `path`.
*/
Expand Down Expand Up @@ -978,6 +984,9 @@ export const makeNoop = (fileSystem: Partial<FileSystem>): FileSystem =>
sink(path) {
return Sink.fail(notFound("sink", path))
},
lstat(path) {
return Effect.fail(notFound("lstat", path))
},
stat(path) {
return Effect.fail(notFound("stat", path))
},
Expand Down
9 changes: 9 additions & 0 deletions packages/platform-node-shared/src/NodeFileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,14 @@ const makeFileInfo = (stat: NFS.Stats): FileSystem.File.Info => ({
blksize: stat.blksize !== undefined ? Option.some(FileSystem.Size(stat.blksize)) : Option.none(),
blocks: Option.fromNullishOr(stat.blocks)
})
const lstat = (() => {
const nodeLstat = effectify(
NFS.lstat,
handleErrnoException("FileSystem", "lstat"),
handleBadArgument("lstat")
)
return (path: string) => Effect.map(nodeLstat(path), makeFileInfo)
})()
const stat = (() => {
const nodeStat = effectify(
NFS.stat,
Expand Down Expand Up @@ -636,6 +644,7 @@ const makeFileSystem = Effect.map(Effect.serviceOption(FileSystem.WatchBackend),
copyFile,
glob,
link,
lstat,
makeDirectory,
makeTempDirectory,
makeTempDirectoryScoped,
Expand Down
17 changes: 17 additions & 0 deletions packages/platform-node-shared/test/NodeFileSystem.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,23 @@ describe("FileSystem", () => {
assert(error.reason._tag === "NotFound")
})))

it("lstat does not follow symbolic links", () =>
runPromise(Effect.scoped(Effect.gen(function*() {
const fs = yield* Fs.FileSystem
const dir = yield* fs.makeTempDirectoryScoped()
const target = `${dir}/target.txt`
const link = `${dir}/link.txt`

yield* fs.writeFileString(target, "target")
yield* fs.symlink(target, link)

const followed = yield* fs.stat(link)
const notFollowed = yield* fs.lstat(link)

assert.strictEqual(followed.type, "File")
assert.strictEqual(notFollowed.type, "SymbolicLink")
}))))

it("truncate", () =>
runPromise(Effect.gen(function*() {
const fs = yield* Fs.FileSystem
Expand Down