Skip to content

Commit 5df29b3

Browse files
committed
feat: FileSystem support for copyFile flags
1 parent 5ab9c08 commit 5df29b3

6 files changed

Lines changed: 113 additions & 8 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"effect": minor
3+
"@effect/platform-node-shared": patch
4+
"@effect/platform-node": patch
5+
"@effect/platform-bun": patch
6+
"@effect/platform-deno": patch
7+
---
8+
9+
Add copy file mode flags to `FileSystem.copyFile`.

packages/effect/src/FileSystem.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,18 @@ export interface FileSystem {
107107
) => Effect.Effect<void, PlatformError>
108108
/**
109109
* Copy a file from `fromPath` to `toPath`.
110+
*
111+
* **Details**
112+
*
113+
* The `mode` option accepts a bitwise combination of {@link CopyFileFlag}
114+
* values.
110115
*/
111116
readonly copyFile: (
112117
fromPath: string,
113-
toPath: string
118+
toPath: string,
119+
options?: {
120+
readonly mode?: number | undefined
121+
}
114122
) => Effect.Effect<void, PlatformError>
115123
/**
116124
* Change the permissions of a file.
@@ -619,6 +627,25 @@ export type OpenFlag =
619627
| "a+"
620628
| "ax+"
621629

630+
/**
631+
* Flags for `FileSystem.copyFile`.
632+
*
633+
* **Details**
634+
*
635+
* Multiple flags can be combined with the bitwise OR operator.
636+
*
637+
* @category models
638+
* @since 4.0.0
639+
*/
640+
export const CopyFileFlag = {
641+
/** Fail if the destination already exists. */
642+
COPYFILE_EXCL: 1,
643+
/** Attempt copy-on-write and fall back to a regular copy if unavailable. */
644+
COPYFILE_FICLONE: 2,
645+
/** Require copy-on-write and fail if unavailable. */
646+
COPYFILE_FICLONE_FORCE: 4
647+
} as const
648+
622649
/**
623650
* Service tag for platform file-system operations.
624651
*

packages/platform-deno/src/DenoFileSystem.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,20 @@ const copy: FileSystem.FileSystem["copy"] = (fromPath, toPath, options) =>
6767
preserveTimestamps: options?.preserveTimestamps ?? false
6868
}))
6969

70-
const copyFile: FileSystem.FileSystem["copyFile"] = (fromPath, toPath) =>
71-
tryPromise("copyFile", fromPath, () => Deno.copyFile(fromPath, toPath))
70+
const copyFile: FileSystem.FileSystem["copyFile"] = (fromPath, toPath, options) => {
71+
const mode = options?.mode ?? 0
72+
const { COPYFILE_EXCL, COPYFILE_FICLONE_FORCE } = FileSystem.CopyFileFlag
73+
if ((mode & COPYFILE_EXCL) !== 0 || (mode & COPYFILE_FICLONE_FORCE) !== 0) {
74+
return Effect.fail(
75+
PlatformError.badArgument({
76+
module: "FileSystem",
77+
method: "copyFile",
78+
description: "The copy file mode is unsupported by Deno"
79+
})
80+
)
81+
}
82+
return tryPromise("copyFile", fromPath, () => Deno.copyFile(fromPath, toPath))
83+
}
7284

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

@@ -485,6 +497,12 @@ const makeFileSystem = Effect.map(Effect.serviceOption(FileSystem.WatchBackend),
485497
/**
486498
* Provides the `FileSystem` service backed by Deno filesystem APIs.
487499
*
500+
* **Gotchas**
501+
*
502+
* `COPYFILE_FICLONE` falls back to a regular copy. `COPYFILE_EXCL` and
503+
* `COPYFILE_FICLONE_FORCE` fail with a PlatformError.BadArgument` because
504+
* Deno cannot provide their semantics.
505+
*
488506
* @category layers
489507
* @since 4.0.0
490508
*/
Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,38 @@
11
import * as DenoFileSystem from "@effect/platform-deno/DenoFileSystem"
2-
import { describe } from "@effect/vitest"
2+
import { assert, describe, it } from "@effect/vitest"
3+
import * as Effect from "effect/Effect"
4+
import * as FileSystem from "effect/FileSystem"
35
import { testLayer } from "../../effect/test/FileSystem.test-utils.ts"
46

5-
describe("FileSystem", () =>
7+
describe("FileSystem", () => {
68
testLayer(DenoFileSystem.layer, {
79
accessOnDirectory: false,
810
tempFileScopedRemovesDirectory: false
9-
}))
11+
})
12+
13+
it.effect("copyFile rejects unsupported copy file modes", () =>
14+
Effect.gen(function*() {
15+
const fs = yield* FileSystem.FileSystem
16+
const root = yield* fs.makeTempDirectoryScoped()
17+
const source = `${root}/source.txt`
18+
const destination = `${root}/destination.txt`
19+
20+
yield* fs.writeFileString(source, "source")
21+
22+
const exclusiveError = yield* Effect.flip(
23+
fs.copyFile(source, destination, { mode: FileSystem.CopyFileFlag.COPYFILE_EXCL })
24+
)
25+
assert.strictEqual(exclusiveError.reason._tag, "BadArgument")
26+
27+
yield* fs.copyFile(source, destination, { mode: FileSystem.CopyFileFlag.COPYFILE_FICLONE })
28+
assert.strictEqual(yield* fs.readFileString(destination), "source")
29+
30+
const forceError = yield* Effect.flip(
31+
fs.copyFile(source, destination, { mode: FileSystem.CopyFileFlag.COPYFILE_FICLONE_FORCE })
32+
)
33+
assert.strictEqual(forceError.reason._tag, "BadArgument")
34+
}).pipe(
35+
Effect.scoped,
36+
Effect.provide(DenoFileSystem.layer)
37+
))
38+
})

packages/platform-node-shared/src/NodeFileSystem.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,13 @@ const copy = ((): FileSystem.FileSystem["copy"] => {
7070

7171
// == copyFile
7272

73-
const copyFile = (() => {
73+
const copyFile: FileSystem.FileSystem["copyFile"] = (() => {
7474
const nodeCopyFile = effectify(
7575
NFS.copyFile,
7676
handleErrnoException("FileSystem", "copyFile"),
7777
handleBadArgument("copyFile")
7878
)
79-
return (fromPath: string, toPath: string) => nodeCopyFile(fromPath, toPath)
79+
return (fromPath, toPath, options) => nodeCopyFile(fromPath, toPath, options?.mode ?? 0)
8080
})()
8181

8282
// == chmod

packages/platform-node-shared/test/NodeFileSystem.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,28 @@ const startWatch = <E, R>(
4444
describe("FileSystem", () => {
4545
testLayer(NodeFileSystem.layer)
4646

47+
it.effect("copyFile supports copy file modes", () =>
48+
Effect.gen(function*() {
49+
const fs = yield* FileSystem.FileSystem
50+
const root = yield* fs.makeTempDirectoryScoped()
51+
const source = `${root}/source.txt`
52+
const destination = `${root}/destination.txt`
53+
54+
yield* fs.writeFileString(source, "source")
55+
yield* fs.writeFileString(destination, "destination")
56+
57+
const error = yield* Effect.flip(
58+
fs.copyFile(source, destination, { mode: FileSystem.CopyFileFlag.COPYFILE_EXCL })
59+
)
60+
assert.strictEqual(error.reason._tag, "AlreadyExists")
61+
62+
yield* fs.copyFile(source, destination, { mode: FileSystem.CopyFileFlag.COPYFILE_FICLONE })
63+
assert.strictEqual(yield* fs.readFileString(destination), "source")
64+
}).pipe(
65+
Effect.scoped,
66+
Effect.provide(NodeFileSystem.layer)
67+
))
68+
4769
it.effect("watch does not report nested changes when recursive is false", () =>
4870
Effect.gen(function*() {
4971
const fs = yield* FileSystem.FileSystem

0 commit comments

Comments
 (0)