Skip to content
Open
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
9 changes: 9 additions & 0 deletions .changeset/quiet-files-copy-flags.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"effect": minor
"@effect/platform-node-shared": patch
"@effect/platform-node": patch
"@effect/platform-bun": patch
"@effect/platform-deno": patch
---

Add copy file mode flags to `FileSystem.copyFile`.
29 changes: 28 additions & 1 deletion packages/effect/src/FileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,18 @@ export interface FileSystem {
) => Effect.Effect<void, PlatformError>
/**
* Copy a file from `fromPath` to `toPath`.
*
* **Details**
*
* The `mode` option accepts a bitwise combination of {@link CopyFileFlag}
* values.
*/
readonly copyFile: (
fromPath: string,
toPath: string
toPath: string,
options?: {
readonly mode?: number | undefined
}
) => Effect.Effect<void, PlatformError>
/**
* Change the permissions of a file.
Expand Down Expand Up @@ -619,6 +627,25 @@ export type OpenFlag =
| "a+"
| "ax+"

/**
* Flags for `FileSystem.copyFile`.
*
* **Details**
*
* Multiple flags can be combined with the bitwise OR operator.
*
* @category models
* @since 4.0.0
*/
export const CopyFileFlag = {
/** Fail if the destination already exists. */
COPYFILE_EXCL: 1,
/** Attempt copy-on-write and fall back to a regular copy if unavailable. */
COPYFILE_FICLONE: 2,
/** Require copy-on-write and fail if unavailable. */
COPYFILE_FICLONE_FORCE: 4
} as const

/**
* Service tag for platform file-system operations.
*
Expand Down
22 changes: 20 additions & 2 deletions packages/platform-deno/src/DenoFileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,20 @@ const copy: FileSystem.FileSystem["copy"] = (fromPath, toPath, options) =>
preserveTimestamps: options?.preserveTimestamps ?? false
}))

const copyFile: FileSystem.FileSystem["copyFile"] = (fromPath, toPath) =>
tryPromise("copyFile", fromPath, () => Deno.copyFile(fromPath, toPath))
const copyFile: FileSystem.FileSystem["copyFile"] = (fromPath, toPath, options) => {
const mode = options?.mode ?? 0
const { COPYFILE_EXCL, COPYFILE_FICLONE_FORCE } = FileSystem.CopyFileFlag
if ((mode & COPYFILE_EXCL) !== 0 || (mode & COPYFILE_FICLONE_FORCE) !== 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want to avoid adding options that are unsupported on some platforms.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is DenoFileSystem not implemented with NodeFileSystem like BunFileSystem? node:fs has the interface for these flags and Deno also implements it - however not correctly (EXCL is not atomic and COPYFILEs ignored).

return Effect.fail(
PlatformError.badArgument({
module: "FileSystem",
method: "copyFile",
description: "The copy file mode is unsupported by Deno"
})
)
}
return tryPromise("copyFile", fromPath, () => Deno.copyFile(fromPath, toPath))
}

const chmod: FileSystem.FileSystem["chmod"] = (path, mode) => tryPromise("chmod", path, () => Deno.chmod(path, mode))

Expand Down Expand Up @@ -485,6 +497,12 @@ const makeFileSystem = Effect.map(Effect.serviceOption(FileSystem.WatchBackend),
/**
* Provides the `FileSystem` service backed by Deno filesystem APIs.
*
* **Gotchas**
*
* `COPYFILE_FICLONE` falls back to a regular copy. `COPYFILE_EXCL` and
* `COPYFILE_FICLONE_FORCE` fail with a PlatformError.BadArgument` because
* Deno cannot provide their semantics.
*
* @category layers
* @since 4.0.0
*/
Expand Down
35 changes: 32 additions & 3 deletions packages/platform-deno/test/DenoFileSystem.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,38 @@
import * as DenoFileSystem from "@effect/platform-deno/DenoFileSystem"
import { describe } from "@effect/vitest"
import { assert, describe, it } from "@effect/vitest"
import * as Effect from "effect/Effect"
import * as FileSystem from "effect/FileSystem"
import { testLayer } from "../../effect/test/FileSystem.test-utils.ts"

describe("FileSystem", () =>
describe("FileSystem", () => {
testLayer(DenoFileSystem.layer, {
accessOnDirectory: false,
tempFileScopedRemovesDirectory: false
}))
})

it.effect("copyFile rejects unsupported copy file modes", () =>
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const root = yield* fs.makeTempDirectoryScoped()
const source = `${root}/source.txt`
const destination = `${root}/destination.txt`

yield* fs.writeFileString(source, "source")

const exclusiveError = yield* Effect.flip(
fs.copyFile(source, destination, { mode: FileSystem.CopyFileFlag.COPYFILE_EXCL })
)
assert.strictEqual(exclusiveError.reason._tag, "BadArgument")

yield* fs.copyFile(source, destination, { mode: FileSystem.CopyFileFlag.COPYFILE_FICLONE })
assert.strictEqual(yield* fs.readFileString(destination), "source")

const forceError = yield* Effect.flip(
fs.copyFile(source, destination, { mode: FileSystem.CopyFileFlag.COPYFILE_FICLONE_FORCE })
)
assert.strictEqual(forceError.reason._tag, "BadArgument")
}).pipe(
Effect.scoped,
Effect.provide(DenoFileSystem.layer)
))
})
4 changes: 2 additions & 2 deletions packages/platform-node-shared/src/NodeFileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,13 @@ const copy = ((): FileSystem.FileSystem["copy"] => {

// == copyFile

const copyFile = (() => {
const copyFile: FileSystem.FileSystem["copyFile"] = (() => {
const nodeCopyFile = effectify(
NFS.copyFile,
handleErrnoException("FileSystem", "copyFile"),
handleBadArgument("copyFile")
)
return (fromPath: string, toPath: string) => nodeCopyFile(fromPath, toPath)
return (fromPath, toPath, options) => nodeCopyFile(fromPath, toPath, options?.mode ?? 0)
})()

// == chmod
Expand Down
22 changes: 22 additions & 0 deletions packages/platform-node-shared/test/NodeFileSystem.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,28 @@ const startWatch = <E, R>(
describe("FileSystem", () => {
testLayer(NodeFileSystem.layer)

it.effect("copyFile supports copy file modes", () =>
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
const root = yield* fs.makeTempDirectoryScoped()
const source = `${root}/source.txt`
const destination = `${root}/destination.txt`

yield* fs.writeFileString(source, "source")
yield* fs.writeFileString(destination, "destination")

const error = yield* Effect.flip(
fs.copyFile(source, destination, { mode: FileSystem.CopyFileFlag.COPYFILE_EXCL })
)
assert.strictEqual(error.reason._tag, "AlreadyExists")

yield* fs.copyFile(source, destination, { mode: FileSystem.CopyFileFlag.COPYFILE_FICLONE })
assert.strictEqual(yield* fs.readFileString(destination), "source")
}).pipe(
Effect.scoped,
Effect.provide(NodeFileSystem.layer)
))

it.effect("watch does not report nested changes when recursive is false", () =>
Effect.gen(function*() {
const fs = yield* FileSystem.FileSystem
Expand Down
Loading