1/**
2 * Defines the portable file system service for Effect programs.
3 *
4 * `FileSystem` is the boundary between Effect code and the host file system.
5 * Platform packages provide concrete layers, while this module defines the
6 * operations for reading, writing, inspecting, streaming, and watching files.
7 * Operations return `Effect`, `Stream`, or `Sink` values and fail with
8 * `PlatformError`. The module also includes file handles, open flags, watch
9 * events, and the watch backend service.
10 *
11 * @since 4.0.0
12 */
13import * as Arr from "./Array.ts"
14import * as ByteSize from "./ByteSize.ts"
15import * as Cause from "./Cause.ts"
16import * as Context from "./Context.ts"
17import * as Effect from "./Effect.ts"
18import { pipe } from "./Function.ts"
19import * as Layer from "./Layer.ts"
20import * as Option from "./Option.ts"
21import { badArgument, type PlatformError, systemError } from "./PlatformError.ts"
22import { hasProperty } from "./Predicate.ts"
23import type * as Pull from "./Pull.ts"
24import type { Scope } from "./Scope.ts"
25import * as Sink from "./Sink.ts"
26import * as Stream from "./Stream.ts"
27
28const TypeId = "~effect/FileSystem"
29
30/**
31 * Core interface for file system operations in Effect.
32 *
33 * **Details**
34 *
35 * The FileSystem interface provides a comprehensive set of file and directory operations
36 * that work cross-platform. All operations return Effect values that can be composed,
37 * transformed, and executed safely with proper error handling.
38 *
39 * **Example** (Accessing file system operations)
40 *
41 * ```ts import.meta.vitest
42 * import { ByteSize, Effect, FileSystem } from "effect"
43 *
44 * const fileSystem = FileSystem.makeNoop({
45 * exists: () => Effect.succeed(true),
46 * makeDirectory: () => Effect.void,
47 * stat: () => Effect.succeed({ size: ByteSize.bytes(22) } as FileSystem.File.Info),
48 * readFileString: () => Effect.succeed("{\"env\": \"development\"}")
49 * })
50 *
51 * const program = Effect.gen(function*() {
52 * const fs = yield* FileSystem.FileSystem
53 *
54 * // Basic file operations
55 * const exists = yield* fs.exists("./config.json")
56 * if (!exists) {
57 * yield* fs.writeFileString("./config.json", "{\"env\": \"development\"}")
58 * }
59 *
60 * // Directory operations
61 * yield* fs.makeDirectory("./logs", { recursive: true })
62 *
63 * // File information
64 * const stats = yield* fs.stat("./config.json")
65 * // Read the file contents
66 * const content = yield* fs.readFileString("./config.json")
67 * return { size: stats.size, content }
68 * })
69 *
70 * const result = Effect.runSync(Effect.provideService(program, FileSystem.FileSystem, fileSystem))
71 * ByteSize.toBigInt(result.size) // => 22n
72 * result.content // => "{\"env\": \"development\"}"
73 * ```
74 *
75 * @category services
76 * @since 4.0.0
77 */
78export interface FileSystem {
79 readonly [TypeId]: typeof TypeId
80
81 /**
82 * Checks whether a file can be accessed.
83 * You can optionally specify the level of access to check for.
84 */
85 readonly access: (
86 path: string,
87 options?: {
88 readonly ok?: boolean | undefined
89 readonly readable?: boolean | undefined
90 readonly writable?: boolean | undefined
91 }
92 ) => Effect.Effect<void, PlatformError>
93 /**
94 * Copy a file or directory from `fromPath` to `toPath`.
95 *
96 * **Details**
97 *
98 * Equivalent to `cp -r`.
99 */
100 readonly copy: (
101 fromPath: string,
102 toPath: string,
103 options?: {
104 readonly overwrite?: boolean | undefined
105 readonly preserveTimestamps?: boolean | undefined
106 }
107 ) => Effect.Effect<void, PlatformError>
108 /**
109 * Copy a file from `fromPath` to `toPath`.
110 */
111 readonly copyFile: (
112 fromPath: string,
113 toPath: string
114 ) => Effect.Effect<void, PlatformError>
115 /**
116 * Change the permissions of a file.
117 */
118 readonly chmod: (
119 path: string,
120 mode: number
121 ) => Effect.Effect<void, PlatformError>
122 /**
123 * Change the owner and group of a file.
124 */
125 readonly chown: (
126 path: string,
127 uid: number,
128 gid: number
129 ) => Effect.Effect<void, PlatformError>
130 /**
131 * Glob a directory.
132 */
133 readonly glob: (
134 pattern: string,
135 options?: {
136 readonly root?: string | undefined
137 readonly exclude?: ReadonlyArray<string> | undefined
138 }
139 ) => Effect.Effect<Array<string>, PlatformError>
140 /**
141 * Checks whether a path exists.
142 */
143 readonly exists: (
144 path: string
145 ) => Effect.Effect<boolean, PlatformError>
146 /**
147 * Create a hard link from `fromPath` to `toPath`.
148 */
149 readonly link: (
150 fromPath: string,
151 toPath: string
152 ) => Effect.Effect<void, PlatformError>
153 /**
154 * Create a directory at `path`. You can optionally specify the mode and
155 * whether to recursively create nested directories.
156 */
157 readonly makeDirectory: (
158 path: string,
159 options?: {
160 readonly recursive?: boolean | undefined
161 readonly mode?: number | undefined
162 }
163 ) => Effect.Effect<void, PlatformError>
164 /**
165 * Create a temporary directory.
166 *
167 * **Details**
168 *
169 * By default the directory will be created inside the system's default
170 * temporary directory, but you can specify a different location by setting
171 * the `directory` option.
172 *
173 * You can also specify a prefix for the directory name by setting the
174 * `prefix` option.
175 */
176 readonly makeTempDirectory: (options?: {
177 readonly directory?: string | undefined
178 readonly prefix?: string | undefined
179 }) => Effect.Effect<string, PlatformError>
180 /**
181 * Create a temporary directory inside a scope.
182 *
183 * **Details**
184 *
185 * Functionally equivalent to `makeTempDirectory`, but the directory will be
186 * automatically deleted when the scope is closed.
187 */
188 readonly makeTempDirectoryScoped: (options?: {
189 readonly directory?: string | undefined
190 readonly prefix?: string | undefined
191 }) => Effect.Effect<string, PlatformError, Scope>
192 /**
193 * Create a temporary file.
194 * The directory creation is functionally equivalent to `makeTempDirectory`.
195 * The file name will be a randomly generated string.
196 */
197 readonly makeTempFile: (options?: {
198 readonly directory?: string | undefined
199 readonly prefix?: string | undefined
200 readonly suffix?: string | undefined
201 }) => Effect.Effect<string, PlatformError>
202 /**
203 * Create a temporary file inside a scope.
204 *
205 * **Details**
206 *
207 * Functionally equivalent to `makeTempFile`, but the file will be
208 * automatically deleted when the scope is closed.
209 */
210 readonly makeTempFileScoped: (options?: {
211 readonly directory?: string | undefined
212 readonly prefix?: string | undefined
213 readonly suffix?: string | undefined
214 }) => Effect.Effect<string, PlatformError, Scope>
215 /**
216 * Open a file at `path` with the specified `options`.
217 *
218 * **Details**
219 *
220 * The file handle will be automatically closed when the scope is closed.
221 */
222 readonly open: (
223 path: string,
224 options?: {
225 readonly flag?: OpenFlag | undefined
226 readonly mode?: number | undefined
227 }
228 ) => Effect.Effect<File, PlatformError, Scope>
229 /**
230 * List the contents of a directory.
231 *
232 * **Details**
233 *
234 * You can recursively list the contents of nested directories by setting the
235 * `recursive` option.
236 */
237 readonly readDirectory: (
238 path: string,
239 options?: {
240 readonly recursive?: boolean | undefined
241 }
242 ) => Effect.Effect<Array<string>, PlatformError>
243 /**
244 * Read the contents of a file.
245 */
246 readonly readFile: (
247 path: string
248 ) => Effect.Effect<Uint8Array, PlatformError>
249 /**
250 * Read the contents of a file.
251 */
252 readonly readFileString: (
253 path: string,
254 encoding?: string
255 ) => Effect.Effect<string, PlatformError>
256 /**
257 * Read the destination of a symbolic link.
258 */
259 readonly readLink: (
260 path: string
261 ) => Effect.Effect<string, PlatformError>
262 /**
263 * Resolve a path to its canonicalized absolute pathname.
264 */
265 readonly realPath: (
266 path: string
267 ) => Effect.Effect<string, PlatformError>
268 /**
269 * Remove a file or directory.
270 */
271 readonly remove: (
272 path: string,
273 options?: {
274 /**
275 * When `true`, you can recursively remove nested directories.
276 */
277 readonly recursive?: boolean | undefined
278 /**
279 * When `true`, exceptions will be ignored if `path` does not exist.
280 */
281 readonly force?: boolean | undefined
282 }
283 ) => Effect.Effect<void, PlatformError>
284 /**
285 * Rename a file or directory.
286 */
287 readonly rename: (
288 oldPath: string,
289 newPath: string
290 ) => Effect.Effect<void, PlatformError>
291 /**
292 * Create a writable `Sink` for the specified `path`.
293 */
294 readonly sink: (
295 path: string,
296 options?: {
297 readonly flag?: OpenFlag | undefined
298 readonly mode?: number | undefined
299 }
300 ) => Sink.Sink<void, Uint8Array, never, PlatformError>
301 /**
302 * Get information about a file at `path`. See `File.Info` for metadata limits.
303 */
304 readonly stat: (
305 path: string
306 ) => Effect.Effect<File.Info, PlatformError>
307 /**
308 * Create a readable `Stream` for the specified `path`.
309 *
310 * **Details**
311 *
312 * Changing the `bufferSize` option will change the internal buffer size of
313 * the stream. It defaults to `4`.
314 *
315 * The `chunkSize` option will change the size of the chunks emitted by the
316 * stream. It defaults to 64kb.
317 *
318 * Changing `offset` and `bytesToRead` will change the offset and the number
319 * of bytes to read from the file.
320 */
321 readonly stream: (
322 path: string,
323 options?: {
324 readonly bytesToRead?: ByteSize.Input | undefined
325 readonly chunkSize?: number | undefined
326 readonly offset?: ByteSize.Input | undefined
327 }
328 ) => Stream.Stream<Uint8Array, PlatformError>
329 /**
330 * Create a symbolic link from `fromPath` to `toPath`.
331 */
332 readonly symlink: (
333 fromPath: string,
334 toPath: string
335 ) => Effect.Effect<void, PlatformError>
336 /**
337 * Truncate a file to a specified length. If the `length` is not specified,
338 * the file will be truncated to length `0`.
339 */
340 readonly truncate: (
341 path: string,
342 length?: number
343 ) => Effect.Effect<void, PlatformError>
344 /**
345 * Change the file system timestamps of the file at `path`.
346 */
347 readonly utimes: (
348 path: string,
349 atime: Date | number,
350 mtime: Date | number
351 ) => Effect.Effect<void, PlatformError>
352 /**
353 * Watch a directory or file for changes.
354 *
355 * **Details**
356 *
357 * By default, only changes to the direct children of the directory are
358 * reported. Set the `recursive` option to `true` to watch for changes in
359 * subdirectories as well.
360 */
361 readonly watch: (path: string, options?: WatchOptions) => Stream.Stream<WatchEvent, PlatformError>
362 /**
363 * Write data to a file at `path`.
364 */
365 readonly writeFile: (
366 path: string,
367 data: Uint8Array,
368 options?: {
369 readonly flag?: OpenFlag | undefined
370 readonly mode?: number | undefined
371 }
372 ) => Effect.Effect<void, PlatformError>
373 /**
374 * Write a string to a file at `path`.
375 */
376 readonly writeFileString: (
377 path: string,
378 data: string,
379 options?: {
380 readonly flag?: OpenFlag | undefined
381 readonly mode?: number | undefined
382 }
383 ) => Effect.Effect<void, PlatformError>
384}
385
386/**
387 * File open flags that determine how a file is opened and what operations are allowed.
388 *
389 * **Details**
390 *
391 * These flags correspond to standard POSIX file open modes and control the file access
392 * permissions and behavior when opening files.
393 *
394 * - `"r"` - Read-only. File must exist.
395 * - `"r+"` - Read/write. File must exist.
396 * - `"w"` - Write-only. Truncates file to zero length or creates new file.
397 * - `"wx"` - Like 'w' but fails if file exists.
398 * - `"w+"` - Read/write. Truncates file to zero length or creates new file.
399 * - `"wx+"` - Like 'w+' but fails if file exists.
400 * - `"a"` - Write-only. Appends to file or creates new file.
401 * - `"ax"` - Like 'a' but fails if file exists.
402 * - `"a+"` - Read/write. Appends to file or creates new file.
403 * - `"ax+"` - Like 'a+' but fails if file exists.
404 *
405 * **Example** (Opening files with flags)
406 *
407 * ```ts import.meta.vitest
408 * import type { FileSystem } from "effect"
409 *
410 * const flags: ReadonlyArray<FileSystem.OpenFlag> = ["r", "w", "a", "r+"]
411 * flags // => ["r", "w", "a", "r+"]
412 * ```
413 *
414 * @category models
415 * @since 4.0.0
416 */
417export type OpenFlag =
418 | "r"
419 | "r+"
420 | "w"
421 | "wx"
422 | "w+"
423 | "wx+"
424 | "a"
425 | "ax"
426 | "a+"
427 | "ax+"
428
429/**
430 * Service tag for platform file-system operations.
431 *
432 * **When to use**
433 *
434 * Use to access or provide operations for files, directories, permissions,
435 * streams, and sinks through the Effect context.
436 *
437 * **Details**
438 *
439 * This key is used to provide and access the FileSystem service in the Effect context.
440 *
441 * **Example** (Accessing and providing FileSystem)
442 *
443 * ```ts import.meta.vitest
444 * import { Effect, FileSystem } from "effect"
445 *
446 * const customFs = FileSystem.makeNoop({
447 * exists: () => Effect.succeed(true),
448 * readFileString: () => Effect.succeed("contents")
449 * })
450 *
451 * // Access the FileSystem service
452 * const program = Effect.gen(function*() {
453 * const fs = yield* FileSystem.FileSystem
454 *
455 * const exists = yield* fs.exists("./data.txt")
456 * return exists ? yield* fs.readFileString("./data.txt") : undefined
457 * })
458 *
459 * const withCustomFs = Effect.provideService(
460 * program,
461 * FileSystem.FileSystem,
462 * customFs
463 * )
464 * Effect.runSync(withCustomFs) // => "contents"
465 * ```
466 *
467 * @category services
468 * @since 4.0.0
469 */
470export const FileSystem: Context.Service<FileSystem, FileSystem> = Context.Service("effect/FileSystem")
471
472/**
473 * Creates a FileSystem implementation from a partial implementation.
474 *
475 * **When to use**
476 *
477 * Use to build a concrete `FileSystem` service from platform-specific core
478 * operations while deriving the convenience methods that can be implemented
479 * from them.
480 *
481 * **Details**
482 *
483 * This function takes a partial FileSystem implementation and automatically provides
484 * default implementations for `exists`, `readFileString`, `stream`, `sink`, and
485 * `writeFileString` methods based on the provided core methods.
486 *
487 * @see {@link makeNoop} for a testing stub that accepts method overrides without requiring a complete implementation
488 * @see {@link layerNoop} for providing a no-op `FileSystem` as a `Layer` in tests
489 *
490 * @category constructors
491 * @since 4.0.0
492 */
493export const make = (
494 impl: Omit<FileSystem, typeof TypeId | "exists" | "readFileString" | "stream" | "sink" | "writeFileString">
495): FileSystem =>
496 FileSystem.of({
497 ...impl,
498 [TypeId]: TypeId,
499 exists: (path) =>
500 pipe(
501 impl.access(path),
502 Effect.as(true),
503 Effect.catchTag(
504 "PlatformError",
505 (e) => e.reason._tag === "NotFound" ? Effect.succeed(false) : Effect.fail(e)
506 )
507 ),
508 readFileString: (path, encoding) =>
509 Effect.flatMap(impl.readFile(path), (_) =>
510 Effect.try({
511 try: () => new TextDecoder(encoding).decode(_),
512 catch: (cause) =>
513 badArgument({
514 module: "FileSystem",
515 method: "readFileString",
516 description: "invalid encoding",
517 cause
518 })
519 })),
520 stream: Effect.fnUntraced(function*(path, options) {
521 const file = yield* impl.open(path, { flag: "r" })
522 const offset = options?.offset === undefined ? undefined : ByteSize.fromInputUnsafe(options.offset)
523 if (offset) {
524 yield* file.seek(offset, "start")
525 }
526 const bytesToRead = options?.bytesToRead === undefined
527 ? undefined
528 : ByteSize.fromInputUnsafe(options.bytesToRead)
529 let totalBytesRead = BigInt(0)
530 // Validate chunk sizes even for zero-byte reads.
531 const chunkSize = Number(BigInt(options?.chunkSize ?? 64 * 1024))
532 const readChunk = file.readAlloc(chunkSize)
533 return Stream.fromPull(Effect.succeed(
534 Effect.flatMap(
535 Effect.suspend((): Pull.Pull<Option.Option<Uint8Array>, PlatformError> => {
536 if (bytesToRead !== undefined && bytesToRead <= totalBytesRead) {
537 return Cause.done()
538 }
539 return bytesToRead !== undefined && (bytesToRead - totalBytesRead) < chunkSize
540 ? file.readAlloc(Number(bytesToRead - totalBytesRead))
541 : readChunk
542 }),
543 Option.match({
544 onNone: () => Cause.done(),
545 onSome: (buf) => {
546 totalBytesRead += BigInt(buf.length)
547 return Effect.succeed(Arr.of(buf))
548 }
549 })
550 )
551 ))
552 }, Stream.unwrap),
553 sink: (path, options) =>
554 pipe(
555 impl.open(path, { ...options, flag: options?.flag ?? "w" }),
556 Effect.map((file) => Sink.forEach((_: Uint8Array) => file.writeAll(_))),
557 Sink.unwrap
558 ),
559 writeFileString: (path, data, options) =>
560 Effect.flatMap(
561 Effect.try({
562 try: () => new TextEncoder().encode(data),
563 catch: (cause) =>
564 badArgument({
565 module: "FileSystem",
566 method: "writeFileString",
567 description: "could not encode string",
568 cause
569 })
570 }),
571 (_) => impl.writeFile(path, _, options)
572 )
573 })
574
575const notFound = (method: string, path: string) =>
576 systemError({
577 module: "FileSystem",
578 method,
579 _tag: "NotFound",
580 description: "No such file or directory",
581 pathOrDescriptor: path
582 })
583
584/**
585 * Creates a stub `FileSystem` implementation for tests.
586 *
587 * **Details**
588 *
589 * By default, `exists` returns `false`, `remove` succeeds, many file operations
590 * fail with `PlatformError` `NotFound`, and temporary-directory/file operations
591 * die as not implemented. Pass method overrides to provide the behavior needed
592 * by a specific test without touching the real file system.
593 *
594 * **Example** (Creating a no-op FileSystem)
595 *
596 * ```ts import.meta.vitest
597 * import { Effect, FileSystem, PlatformError } from "effect"
598 *
599 * // Create a test filesystem that only allows reading specific files
600 * const testFs = FileSystem.makeNoop({
601 * readFileString: (path) => {
602 * if (path === "test-config.json") {
603 * return Effect.succeed("{\"test\": true}")
604 * }
605 * return Effect.fail(
606 * PlatformError.systemError({
607 * _tag: "NotFound",
608 * module: "FileSystem",
609 * method: "readFileString",
610 * description: "File not found",
611 * pathOrDescriptor: path
612 * })
613 * )
614 * },
615 * exists: (path) => Effect.succeed(path === "test-config.json")
616 * })
617 *
618 * // Use in tests
619 * const program = Effect.gen(function*() {
620 * const content = yield* testFs.readFileString("test-config.json")
621 * return content
622 * })
623 *
624 * // Test with the no-op filesystem
625 * const testProgram = Effect.provideService(
626 * program,
627 * FileSystem.FileSystem,
628 * testFs
629 * )
630 * Effect.runSync(testProgram) // => "{\"test\": true}"
631 * ```
632 *
633 * @category constructors
634 * @since 4.0.0
635 */
636export const makeNoop = (fileSystem: Partial<FileSystem>): FileSystem =>
637 FileSystem.of({
638 [TypeId]: TypeId,
639 access(path) {
640 return Effect.fail(notFound("access", path))
641 },
642 chmod(path) {
643 return Effect.fail(notFound("chmod", path))
644 },
645 chown(path) {
646 return Effect.fail(notFound("chown", path))
647 },
648 copy(path) {
649 return Effect.fail(notFound("copy", path))
650 },
651 copyFile(path) {
652 return Effect.fail(notFound("copyFile", path))
653 },
654 glob(pattern) {
655 return Effect.fail(notFound("glob", pattern))
656 },
657 exists() {
658 return Effect.succeed(false)
659 },
660 link(path) {
661 return Effect.fail(notFound("link", path))
662 },
663 makeDirectory() {
664 return Effect.die("not implemented")
665 },
666 makeTempDirectory() {
667 return Effect.die("not implemented")
668 },
669 makeTempDirectoryScoped() {
670 return Effect.die("not implemented")
671 },
672 makeTempFile() {
673 return Effect.die("not implemented")
674 },
675 makeTempFileScoped() {
676 return Effect.die("not implemented")
677 },
678 open(path) {
679 return Effect.fail(notFound("open", path))
680 },
681 readDirectory(path) {
682 return Effect.fail(notFound("readDirectory", path))
683 },
684 readFile(path) {
685 return Effect.fail(notFound("readFile", path))
686 },
687 readFileString(path) {
688 return Effect.fail(notFound("readFileString", path))
689 },
690 readLink(path) {
691 return Effect.fail(notFound("readLink", path))
692 },
693 realPath(path) {
694 return Effect.fail(notFound("realPath", path))
695 },
696 remove() {
697 return Effect.void
698 },
699 rename(oldPath) {
700 return Effect.fail(notFound("rename", oldPath))
701 },
702 sink(path) {
703 return Sink.fail(notFound("sink", path))
704 },
705 stat(path) {
706 return Effect.fail(notFound("stat", path))
707 },
708 stream(path) {
709 return Stream.fail(notFound("stream", path))
710 },
711 symlink(fromPath) {
712 return Effect.fail(notFound("symlink", fromPath))
713 },
714 truncate(path) {
715 return Effect.fail(notFound("truncate", path))
716 },
717 utimes(path) {
718 return Effect.fail(notFound("utimes", path))
719 },
720 watch(path) {
721 return Stream.fail(notFound("watch", path))
722 },
723 writeFile(path) {
724 return Effect.fail(notFound("writeFile", path))
725 },
726 writeFileString(path) {
727 return Effect.fail(notFound("writeFileString", path))
728 },
729 ...fileSystem
730 })
731
732/**
733 * Creates a Layer that provides a no-op FileSystem implementation for testing.
734 *
735 * **Details**
736 *
737 * This is a convenience function that wraps `makeNoop` in a Layer, making it easy
738 * to provide the test filesystem to your Effect programs.
739 *
740 * **Example** (Providing a no-op FileSystem layer)
741 *
742 * ```ts import.meta.vitest
743 * import { Effect, FileSystem } from "effect"
744 *
745 * // Create a test layer with specific behaviors
746 * const testLayer = FileSystem.layerNoop({
747 * readFileString: (path) => Effect.succeed("mocked content"),
748 * exists: () => Effect.succeed(true)
749 * })
750 *
751 * const program = Effect.gen(function*() {
752 * const fs = yield* FileSystem.FileSystem
753 * const content = yield* fs.readFileString("any-file.txt")
754 * return content
755 * })
756 *
757 * // Provide the test layer
758 * const testProgram = Effect.provide(program, testLayer)
759 * Effect.runSync(testProgram) // => "mocked content"
760 * ```
761 *
762 * @category layers
763 * @since 4.0.0
764 */
765export const layerNoop = (fileSystem: Partial<FileSystem>): Layer.Layer<FileSystem> =>
766 Layer.succeed(FileSystem)(makeNoop(fileSystem))
767
768/**
769 * Runtime type identifier attached to `FileSystem.File` handles and used by
770 * `isFile` to recognize them.
771 *
772 * **Details**
773 *
774 * This marker is part of the runtime representation of file handles. Prefer
775 * `isFile` when narrowing unknown values.
776 *
777 * @see {@link File} for the open file handle shape that carries this marker
778 * @see {@link isFile} for the public guard that checks this marker
779 *
780 * @category type IDs
781 * @since 4.0.0
782 */
783export const FileTypeId = "~effect/FileSystem/File"
784
785/**
786 * Returns `true` if a value is a `File` handle by checking for the
787 * `FileTypeId` marker.
788 *
789 * **When to use**
790 *
791 * Use when accepting an unknown value and you need to narrow it to a `File`
792 * before calling file-handle operations.
793 *
794 * **Details**
795 *
796 * This is a structural marker check. It does not validate the marker value or
797 * the shape of the file handle.
798 *
799 * @see {@link File} for the file-handle interface narrowed by this guard
800 * @see {@link FileTypeId} for the runtime marker checked by this guard
801 *
802 * @category guards
803 * @since 4.0.0
804 */
805export const isFile = (u: unknown): u is File => hasProperty(u, FileTypeId)
806
807/**
808 * Interface representing an open file handle.
809 *
810 * **Details**
811 *
812 * Provides low-level file operations including reading, writing, seeking,
813 * and retrieving file information. File handles are automatically managed
814 * within scoped operations to ensure proper cleanup.
815 *
816 * Each handle tracks its own cursor, which can differ from the POSIX file
817 * offset. In append mode, writes go to the end of the file without moving the
818 * cursor. Outside append mode, `truncate` clamps a cursor past the new length
819 * to that length.
820 *
821 * **Example** (Working with file handles)
822 *
823 * ```ts import.meta.vitest
824 * import { ByteSize, Effect, FileSystem, Option } from "effect"
825 *
826 * const file: FileSystem.File = {
827 * [FileSystem.FileTypeId]: FileSystem.FileTypeId,
828 * stat: Effect.succeed({ size: ByteSize.bytes(5) } as FileSystem.File.Info),
829 * seek: () => Effect.succeed(BigInt(0)),
830 * sync: Effect.void,
831 * read: (buffer) => Effect.sync(() => {
832 * buffer.set([1, 2, 3, 4, 5])
833 * return 5
834 * }),
835 * readAlloc: () => Effect.succeed(Option.none()),
836 * truncate: () => Effect.void,
837 * write: (buffer) => Effect.succeed(buffer.length),
838 * writeAll: () => Effect.void
839 * }
840 *
841 * const program = Effect.gen(function*() {
842 * const stats = yield* file.stat
843 * const buffer = new Uint8Array(5)
844 * const bytesRead = yield* file.read(buffer)
845 * yield* file.writeAll(new TextEncoder().encode("Hello"))
846 * yield* file.sync
847 * return { size: stats.size, bytesRead, buffer: Array.from(buffer) }
848 * })
849 *
850 * const result = Effect.runSync(program)
851 * ByteSize.toBigInt(result.size) // => 5n
852 * result.bytesRead // => 5
853 * result.buffer // => [1, 2, 3, 4, 5]
854 * ```
855 *
856 * @category models
857 * @since 4.0.0
858 */
859export interface File {
860 readonly [FileTypeId]: typeof FileTypeId
861 /**
862 * Get information about the open file. See `File.Info` for metadata limits.
863 */
864 readonly stat: Effect.Effect<File.Info, PlatformError>
865 /**
866 * Seeks before the start fail with `BadArgument` and leave the cursor unchanged.
867 */
868 readonly seek: (offset: bigint, from: SeekMode) => Effect.Effect<bigint, PlatformError>
869 readonly sync: Effect.Effect<void, PlatformError>
870 readonly read: (buffer: Uint8Array) => Effect.Effect<number, PlatformError>
871 readonly readAlloc: (size: number) => Effect.Effect<Option.Option<Uint8Array>, PlatformError>
872 readonly truncate: (length?: number) => Effect.Effect<void, PlatformError>
873 readonly write: (buffer: Uint8Array) => Effect.Effect<number, PlatformError>
874 readonly writeAll: (buffer: Uint8Array) => Effect.Effect<void, PlatformError>
875}
876
877/**
878 * Namespace containing types associated with open file handles, including file
879 * descriptors, entry kinds, and stat information.
880 *
881 * @since 4.0.0
882 */
883export declare namespace File {
884 /**
885 * Enumeration of possible file system entry types.
886 *
887 * **Details**
888 *
889 * Represents the different types of entries that can exist in a file system,
890 * from regular files to special device files and symbolic links.
891 *
892 * @category models
893 * @since 4.0.0
894 */
895 export type Type =
896 | "File"
897 | "Directory"
898 | "SymbolicLink"
899 | "BlockDevice"
900 | "CharacterDevice"
901 | "FIFO"
902 | "Socket"
903 | "Unknown"
904
905 /**
906 * Comprehensive file information structure.
907 *
908 * **Details**
909 *
910 * Contains metadata about a file or directory including type, timestamps,
911 * permissions, and size information. This structure is returned by file
912 * stat operations.
913 *
914 * Node and Bun preserve `size` and `blksize` exactly. Optional `number`
915 * metadata is `Option.none()` when absent or outside the safe integer range.
916 * Unsafe `dev` or `mode` values fail the stat operation with `BadArgument`.
917 *
918 * **Example** (Inspecting file information)
919 *
920 * ```ts import.meta.vitest
921 * import { ByteSize, FileSystem, Option } from "effect"
922 *
923 * const info: FileSystem.File.Info = {
924 * type: "File",
925 * mtime: Option.none(),
926 * atime: Option.none(),
927 * birthtime: Option.none(),
928 * dev: 1,
929 * ino: Option.none(),
930 * mode: 0o644,
931 * nlink: Option.none(),
932 * uid: Option.none(),
933 * gid: Option.none(),
934 * rdev: Option.none(),
935 * size: ByteSize.bytes(5),
936 * blksize: Option.none(),
937 * blocks: Option.none()
938 * }
939 *
940 * info.type // => "File"
941 * ByteSize.toBigInt(info.size) // => 5n
942 * info.mode.toString(8) // => "644"
943 *
944 * const modified = Option.match(info.mtime, {
945 * onNone: () => "unavailable",
946 * onSome: (mtime) => mtime.toISOString()
947 * })
948 * modified // => "unavailable"
949 * info.type === "File" // => true
950 * ```
951 *
952 * @category models
953 * @since 4.0.0
954 */
955 export interface Info {
956 readonly type: Type
957 readonly mtime: Option.Option<Date>
958 readonly atime: Option.Option<Date>
959 readonly birthtime: Option.Option<Date>
960 readonly dev: number
961 readonly ino: Option.Option<number>
962 readonly mode: number
963 readonly nlink: Option.Option<number>
964 readonly uid: Option.Option<number>
965 readonly gid: Option.Option<number>
966 readonly rdev: Option.Option<number>
967 readonly size: ByteSize.ByteSize
968 readonly blksize: Option.Option<ByteSize.ByteSize>
969 readonly blocks: Option.Option<number>
970 }
971}
972
973/**
974 * Specifies the reference point for seeking within an open file.
975 *
976 * **When to use**
977 *
978 * Use with `File` handles when positioning the cursor before a read or write
979 * and the offset must be interpreted from either the start of the file or the
980 * current cursor.
981 *
982 * **Details**
983 *
984 * - `"start"` seeks from the beginning of the file.
985 * - `"current"` seeks from the current cursor position.
986 *
987 * @see {@link File} for the open file handle API whose `seek` method consumes this mode
988 *
989 * @category models
990 * @since 4.0.0
991 */
992export type SeekMode = "start" | "current"
993
994/**
995 * Options for watching files or directories.
996 *
997 * @category models
998 * @since 4.0.0
999 */
1000export interface WatchOptions {
1001 /**
1002 * When `true`, changes in subdirectories are also reported.
1003 */
1004 readonly recursive?: boolean | undefined
1005}
1006
1007/**
1008 * Represents file system events emitted when watching files or directories.
1009 *
1010 * **When to use**
1011 *
1012 * Use when consuming file system watch streams and pattern matching on `_tag`
1013 * to handle created, updated, or removed paths.
1014 *
1015 * **Details**
1016 *
1017 * The union covers create, update, and remove events. Each event carries the
1018 * reported `path`.
1019 *
1020 * @see {@link FileSystem} for the service interface whose `watch` operation emits these events
1021 *
1022 * @category models
1023 * @since 4.0.0
1024 */
1025export type WatchEvent = WatchEvent.Create | WatchEvent.Update | WatchEvent.Remove
1026
1027/**
1028 * Namespace containing the concrete event shapes emitted by `FileSystem.watch`.
1029 *
1030 * @since 4.0.0
1031 */
1032export declare namespace WatchEvent {
1033 /**
1034 * Event representing the creation of a new file or directory.
1035 *
1036 * **Details**
1037 *
1038 * This event is triggered when a new file or directory is created
1039 * in the watched location.
1040 *
1041 * @category models
1042 * @since 4.0.0
1043 */
1044 export interface Create {
1045 readonly _tag: "Create"
1046 readonly path: string
1047 }
1048
1049 /**
1050 * Event representing the modification of an existing file or directory.
1051 *
1052 * **Details**
1053 *
1054 * This event is triggered when an existing file or directory is
1055 * modified in the watched location.
1056 *
1057 * @category models
1058 * @since 4.0.0
1059 */
1060 export interface Update {
1061 readonly _tag: "Update"
1062 readonly path: string
1063 }
1064
1065 /**
1066 * Event representing the deletion of a file or directory.
1067 *
1068 * **Details**
1069 *
1070 * This event is triggered when a file or directory is deleted
1071 * from the watched location.
1072 *
1073 * @category models
1074 * @since 4.0.0
1075 */
1076 export interface Remove {
1077 readonly _tag: "Remove"
1078 readonly path: string
1079 }
1080}
1081
1082/**
1083 * Service key for file system watch backend implementations.
1084 *
1085 * **Details**
1086 *
1087 * This service provides the low-level file watching capabilities that can be
1088 * implemented differently on various platforms (e.g., inotify on Linux,
1089 * FSEvents on macOS, etc.).
1090 *
1091 * **Example** (Providing a custom watch backend)
1092 *
1093 * ```ts import.meta.vitest
1094 * import { Effect, FileSystem, Option, Stream } from "effect"
1095 *
1096 * // Custom watch backend implementation
1097 * const customWatchBackend = {
1098 * register: (path: string, stat: FileSystem.File.Info) => {
1099 * // Implementation would depend on platform
1100 * return Option.some(Stream.empty) // Placeholder implementation
1101 * }
1102 * }
1103 *
1104 * const program = Effect.gen(function*() {
1105 * const backend = yield* FileSystem.WatchBackend
1106 * return Option.isSome(
1107 * backend.register("./directory", { type: "Directory" } as FileSystem.File.Info)
1108 * )
1109 * })
1110 *
1111 * const withCustomBackend = Effect.provideService(
1112 * program,
1113 * FileSystem.WatchBackend,
1114 * customWatchBackend
1115 * )
1116 * Effect.runSync(withCustomBackend) // => true
1117 * ```
1118 *
1119 * @category services
1120 * @since 4.0.0
1121 */
1122export class WatchBackend extends Context.Service<WatchBackend, {
1123 readonly register: (
1124 path: string,
1125 stat: File.Info,
1126 options?: WatchOptions
1127 ) => Option.Option<Stream.Stream<WatchEvent, PlatformError>>
1128}>()("effect/FileSystem/WatchBackend") {}