/** * Defines the portable file system service for Effect programs. * * `FileSystem` is the boundary between Effect code and the host file system. * Platform packages provide concrete layers, while this module defines the * operations for reading, writing, inspecting, streaming, and watching files. * Operations return `Effect`, `Stream`, or `Sink` values and fail with * `PlatformError`. The module also includes file handles, open flags, watch * events, and the watch backend service. * * @stability unstable * @since 4.0.0 */ import * as Arr from "./Array.ts" import * as ByteSize from "./ByteSize.ts" import * as Cause from "./Cause.ts" import * as Context from "./Context.ts" import * as Effect from "./Effect.ts" import { pipe } from "./Function.ts" import * as Layer from "./Layer.ts" import * as Option from "./Option.ts" import { badArgument, type PlatformError, systemError } from "./PlatformError.ts" import { hasProperty } from "./Predicate.ts" import type * as Pull from "./Pull.ts" import type { Scope } from "./Scope.ts" import * as Sink from "./Sink.ts" import * as Stream from "./Stream.ts" const TypeId = "~effect/FileSystem" /** * Core interface for file system operations in Effect. * * **Details** * * The FileSystem interface provides a comprehensive set of file and directory operations * that work cross-platform. All operations return Effect values that can be composed, * transformed, and executed safely with proper error handling. * * **Example** (Accessing file system operations) * * ```ts import.meta.vitest * import { ByteSize, Effect, FileSystem } from "effect" * * const fileSystem = FileSystem.makeNoop({ * exists: () => Effect.succeed(true), * makeDirectory: () => Effect.void, * stat: () => Effect.succeed({ size: ByteSize.bytes(22) } as FileSystem.File.Info), * readFileString: () => Effect.succeed("{\"env\": \"development\"}") * }) * * const program = Effect.gen(function*() { * const fs = yield* FileSystem.FileSystem * * // Basic file operations * const exists = yield* fs.exists("./config.json") * if (!exists) { * yield* fs.writeFileString("./config.json", "{\"env\": \"development\"}") * } * * // Directory operations * yield* fs.makeDirectory("./logs", { recursive: true }) * * // File information * const stats = yield* fs.stat("./config.json") * // Read the file contents * const content = yield* fs.readFileString("./config.json") * return { size: stats.size, content } * }) * * const result = Effect.runSync(Effect.provideService(program, FileSystem.FileSystem, fileSystem)) * ByteSize.toBigInt(result.size) // => 22n * result.content // => "{\"env\": \"development\"}" * ``` * * @stability unstable * @category services * @since 4.0.0 */ export interface FileSystem { readonly [TypeId]: typeof TypeId /** * Checks whether a file can be accessed. * You can optionally specify the level of access to check for. */ readonly access: ( path: string, options?: { readonly ok?: boolean | undefined readonly readable?: boolean | undefined readonly writable?: boolean | undefined } ) => Effect.Effect /** * Copy a file or directory from `fromPath` to `toPath`. * * **Details** * * Equivalent to `cp -r`. */ readonly copy: ( fromPath: string, toPath: string, options?: { readonly overwrite?: boolean | undefined readonly preserveTimestamps?: boolean | undefined } ) => Effect.Effect /** * Copy a file from `fromPath` to `toPath`. */ readonly copyFile: ( fromPath: string, toPath: string ) => Effect.Effect /** * Change the permissions of a file. */ readonly chmod: ( path: string, mode: number ) => Effect.Effect /** * Change the owner and group of a file. */ readonly chown: ( path: string, uid: number, gid: number ) => Effect.Effect /** * Glob a directory. */ readonly glob: ( pattern: string, options?: { readonly root?: string | undefined readonly exclude?: ReadonlyArray | undefined } ) => Effect.Effect, PlatformError> /** * Checks whether a path exists. */ readonly exists: ( path: string ) => Effect.Effect /** * Create a hard link from `fromPath` to `toPath`. */ readonly link: ( fromPath: string, toPath: string ) => Effect.Effect /** * Get information about a file at `path` without following symbolic links. * See `File.Info` for metadata limits. */ readonly lstat: ( path: string ) => Effect.Effect /** * Create a directory at `path`. You can optionally specify the mode and * whether to recursively create nested directories. */ readonly makeDirectory: ( path: string, options?: { readonly recursive?: boolean | undefined readonly mode?: number | undefined } ) => Effect.Effect /** * Create a temporary directory. * * **Details** * * By default the directory will be created inside the system's default * temporary directory, but you can specify a different location by setting * the `directory` option. * * You can also specify a prefix for the directory name by setting the * `prefix` option. */ readonly makeTempDirectory: (options?: { readonly directory?: string | undefined readonly prefix?: string | undefined }) => Effect.Effect /** * Create a temporary directory inside a scope. * * **Details** * * Functionally equivalent to `makeTempDirectory`, but the directory will be * automatically deleted when the scope is closed. */ readonly makeTempDirectoryScoped: (options?: { readonly directory?: string | undefined readonly prefix?: string | undefined }) => Effect.Effect /** * Create a temporary file. * The directory creation is functionally equivalent to `makeTempDirectory`. * The file name will be a randomly generated string. */ readonly makeTempFile: (options?: { readonly directory?: string | undefined readonly prefix?: string | undefined readonly suffix?: string | undefined }) => Effect.Effect /** * Create a temporary file inside a scope. * * **Details** * * Functionally equivalent to `makeTempFile`, but the file will be * automatically deleted when the scope is closed. */ readonly makeTempFileScoped: (options?: { readonly directory?: string | undefined readonly prefix?: string | undefined readonly suffix?: string | undefined }) => Effect.Effect /** * Open a file at `path` with the specified `options`. * * **Details** * * The file handle will be automatically closed when the scope is closed. * * `noFollow` rejects symlinks at the final path component, not in parent * directories. Node and Bun support it on POSIX; unsupported platforms, * including Windows and Deno, fail with `BadArgument`. */ readonly open: ( path: string, options?: { readonly flag?: OpenFlag | undefined readonly mode?: number | undefined readonly noFollow?: boolean | undefined } ) => Effect.Effect /** * List the contents of a directory. * * **Details** * * You can recursively list the contents of nested directories by setting the * `recursive` option. */ readonly readDirectory: ( path: string, options?: { readonly recursive?: boolean | undefined } ) => Effect.Effect, PlatformError> /** * Read the contents of a file. */ readonly readFile: ( path: string ) => Effect.Effect /** * Read the contents of a file. */ readonly readFileString: ( path: string, encoding?: string ) => Effect.Effect /** * Read the destination of a symbolic link. */ readonly readLink: ( path: string ) => Effect.Effect /** * Resolve a path to its canonicalized absolute pathname. */ readonly realPath: ( path: string ) => Effect.Effect /** * Remove a file or directory. */ readonly remove: ( path: string, options?: { /** * When `true`, you can recursively remove nested directories. */ readonly recursive?: boolean | undefined /** * When `true`, exceptions will be ignored if `path` does not exist. */ readonly force?: boolean | undefined } ) => Effect.Effect /** * Rename a file or directory. */ readonly rename: ( oldPath: string, newPath: string ) => Effect.Effect /** * Create a writable `Sink` for the specified `path`. */ readonly sink: ( path: string, options?: { readonly flag?: OpenFlag | undefined readonly mode?: number | undefined } ) => Sink.Sink /** * Get information about a file at `path`. See `File.Info` for metadata limits. */ readonly stat: ( path: string ) => Effect.Effect /** * Create a readable `Stream` for the specified `path`. * * **Details** * * Changing the `bufferSize` option will change the internal buffer size of * the stream. It defaults to `4`. * * The `chunkSize` option will change the size of the chunks emitted by the * stream. It defaults to 64kb. * * Changing `offset` and `bytesToRead` will change the offset and the number * of bytes to read from the file. */ readonly stream: ( path: string, options?: { readonly bytesToRead?: ByteSize.Input | undefined readonly chunkSize?: number | undefined readonly offset?: ByteSize.Input | undefined } ) => Stream.Stream /** * Create a symbolic link from `fromPath` to `toPath`. */ readonly symlink: ( fromPath: string, toPath: string ) => Effect.Effect /** * Truncate a file to a specified length. If the `length` is not specified, * the file will be truncated to length `0`. */ readonly truncate: ( path: string, length?: number ) => Effect.Effect /** * Change the file system timestamps of the file at `path`. */ readonly utimes: ( path: string, atime: Date | number, mtime: Date | number ) => Effect.Effect /** * Watch a directory or file for changes. * * **Details** * * By default, only changes to the direct children of the directory are * reported. Set the `recursive` option to `true` to watch for changes in * subdirectories as well. */ readonly watch: (path: string, options?: WatchOptions) => Stream.Stream /** * Write data to a file at `path`. */ readonly writeFile: ( path: string, data: Uint8Array, options?: { readonly flag?: OpenFlag | undefined readonly mode?: number | undefined } ) => Effect.Effect /** * Write a string to a file at `path`. */ readonly writeFileString: ( path: string, data: string, options?: { readonly flag?: OpenFlag | undefined readonly mode?: number | undefined } ) => Effect.Effect } /** * File open flags that determine how a file is opened and what operations are allowed. * * **Details** * * These flags correspond to standard POSIX file open modes and control the file access * permissions and behavior when opening files. * * - `"r"` - Read-only. File must exist. * - `"r+"` - Read/write. File must exist. * - `"w"` - Write-only. Truncates file to zero length or creates new file. * - `"wx"` - Like 'w' but fails if file exists. * - `"w+"` - Read/write. Truncates file to zero length or creates new file. * - `"wx+"` - Like 'w+' but fails if file exists. * - `"a"` - Write-only. Appends to file or creates new file. * - `"ax"` - Like 'a' but fails if file exists. * - `"a+"` - Read/write. Appends to file or creates new file. * - `"ax+"` - Like 'a+' but fails if file exists. * * **Example** (Opening files with flags) * * ```ts import.meta.vitest * import type { FileSystem } from "effect" * * const flags: ReadonlyArray = ["r", "w", "a", "r+"] * flags // => ["r", "w", "a", "r+"] * ``` * * @stability unstable * @category models * @since 4.0.0 */ export type OpenFlag = | "r" | "r+" | "w" | "wx" | "w+" | "wx+" | "a" | "ax" | "a+" | "ax+" /** * Service tag for platform file-system operations. * * **When to use** * * Use to access or provide operations for files, directories, permissions, * streams, and sinks through the Effect context. * * **Details** * * This key is used to provide and access the FileSystem service in the Effect context. * * **Example** (Accessing and providing FileSystem) * * ```ts import.meta.vitest * import { Effect, FileSystem } from "effect" * * const customFs = FileSystem.makeNoop({ * exists: () => Effect.succeed(true), * readFileString: () => Effect.succeed("contents") * }) * * // Access the FileSystem service * const program = Effect.gen(function*() { * const fs = yield* FileSystem.FileSystem * * const exists = yield* fs.exists("./data.txt") * return exists ? yield* fs.readFileString("./data.txt") : undefined * }) * * const withCustomFs = Effect.provideService( * program, * FileSystem.FileSystem, * customFs * ) * Effect.runSync(withCustomFs) // => "contents" * ``` * * @stability unstable * @category services * @since 4.0.0 */ export const FileSystem: Context.Service = Context.Service("effect/FileSystem") /** * Creates a FileSystem implementation from a partial implementation. * * **When to use** * * Use to build a concrete `FileSystem` service from platform-specific core * operations while deriving the convenience methods that can be implemented * from them. * * **Details** * * This function takes a partial FileSystem implementation and automatically provides * default implementations for `exists`, `readFileString`, `stream`, `sink`, and * `writeFileString` methods based on the provided core methods. * * @see {@link makeNoop} for a testing stub that accepts method overrides without requiring a complete implementation * @see {@link layerNoop} for providing a no-op `FileSystem` as a `Layer` in tests * * @stability unstable * @category constructors * @since 4.0.0 */ export const make = ( impl: Omit ): FileSystem => FileSystem.of({ ...impl, [TypeId]: TypeId, exists: (path) => pipe( impl.access(path), Effect.as(true), Effect.catchTag( "PlatformError", (e) => e.reason._tag === "NotFound" ? Effect.succeed(false) : Effect.fail(e) ) ), readFileString: (path, encoding) => Effect.flatMap(impl.readFile(path), (_) => Effect.try({ try: () => new TextDecoder(encoding).decode(_), catch: (cause) => badArgument({ module: "FileSystem", method: "readFileString", description: "invalid encoding", cause }) })), stream: Effect.fnUntraced(function*(path, options) { const file = yield* impl.open(path, { flag: "r" }) const offset = options?.offset === undefined ? undefined : ByteSize.fromInputUnsafe(options.offset) if (offset) { yield* file.seek(offset, "start") } const bytesToRead = options?.bytesToRead === undefined ? undefined : ByteSize.fromInputUnsafe(options.bytesToRead) let totalBytesRead = BigInt(0) // Validate chunk sizes even for zero-byte reads. const chunkSize = Number(BigInt(options?.chunkSize ?? 64 * 1024)) const readChunk = file.readAlloc(chunkSize) return Stream.fromPull(Effect.succeed( Effect.flatMap( Effect.suspend((): Pull.Pull, PlatformError> => { if (bytesToRead !== undefined && bytesToRead <= totalBytesRead) { return Cause.done() } return bytesToRead !== undefined && (bytesToRead - totalBytesRead) < chunkSize ? file.readAlloc(Number(bytesToRead - totalBytesRead)) : readChunk }), Option.match({ onNone: () => Cause.done(), onSome: (buf) => { totalBytesRead += BigInt(buf.length) return Effect.succeed(Arr.of(buf)) } }) ) )) }, Stream.unwrap), sink: (path, options) => pipe( impl.open(path, { ...options, flag: options?.flag ?? "w" }), Effect.map((file) => Sink.forEach((_: Uint8Array) => file.writeAll(_))), Sink.unwrap ), writeFileString: (path, data, options) => Effect.flatMap( Effect.try({ try: () => new TextEncoder().encode(data), catch: (cause) => badArgument({ module: "FileSystem", method: "writeFileString", description: "could not encode string", cause }) }), (_) => impl.writeFile(path, _, options) ) }) const notFound = (method: string, path: string) => systemError({ module: "FileSystem", method, _tag: "NotFound", description: "No such file or directory", pathOrDescriptor: path }) /** * Creates a stub `FileSystem` implementation for tests. * * **Details** * * By default, `exists` returns `false`, `remove` succeeds, many file operations * fail with `PlatformError` `NotFound`, and temporary-directory/file operations * die as not implemented. Pass method overrides to provide the behavior needed * by a specific test without touching the real file system. * * **Example** (Creating a no-op FileSystem) * * ```ts import.meta.vitest * import { Effect, FileSystem, PlatformError } from "effect" * * // Create a test filesystem that only allows reading specific files * const testFs = FileSystem.makeNoop({ * readFileString: (path) => { * if (path === "test-config.json") { * return Effect.succeed("{\"test\": true}") * } * return Effect.fail( * PlatformError.systemError({ * _tag: "NotFound", * module: "FileSystem", * method: "readFileString", * description: "File not found", * pathOrDescriptor: path * }) * ) * }, * exists: (path) => Effect.succeed(path === "test-config.json") * }) * * // Use in tests * const program = Effect.gen(function*() { * const content = yield* testFs.readFileString("test-config.json") * return content * }) * * // Test with the no-op filesystem * const testProgram = Effect.provideService( * program, * FileSystem.FileSystem, * testFs * ) * Effect.runSync(testProgram) // => "{\"test\": true}" * ``` * * @stability unstable * @category constructors * @since 4.0.0 */ export const makeNoop = (fileSystem: Partial): FileSystem => FileSystem.of({ [TypeId]: TypeId, access(path) { return Effect.fail(notFound("access", path)) }, chmod(path) { return Effect.fail(notFound("chmod", path)) }, chown(path) { return Effect.fail(notFound("chown", path)) }, copy(path) { return Effect.fail(notFound("copy", path)) }, copyFile(path) { return Effect.fail(notFound("copyFile", path)) }, glob(pattern) { return Effect.fail(notFound("glob", pattern)) }, exists() { return Effect.succeed(false) }, link(path) { return Effect.fail(notFound("link", path)) }, lstat(path) { return Effect.fail(notFound("lstat", path)) }, makeDirectory() { return Effect.die("not implemented") }, makeTempDirectory() { return Effect.die("not implemented") }, makeTempDirectoryScoped() { return Effect.die("not implemented") }, makeTempFile() { return Effect.die("not implemented") }, makeTempFileScoped() { return Effect.die("not implemented") }, open(path) { return Effect.fail(notFound("open", path)) }, readDirectory(path) { return Effect.fail(notFound("readDirectory", path)) }, readFile(path) { return Effect.fail(notFound("readFile", path)) }, readFileString(path) { return Effect.fail(notFound("readFileString", path)) }, readLink(path) { return Effect.fail(notFound("readLink", path)) }, realPath(path) { return Effect.fail(notFound("realPath", path)) }, remove() { return Effect.void }, rename(oldPath) { return Effect.fail(notFound("rename", oldPath)) }, sink(path) { return Sink.fail(notFound("sink", path)) }, stat(path) { return Effect.fail(notFound("stat", path)) }, stream(path) { return Stream.fail(notFound("stream", path)) }, symlink(fromPath) { return Effect.fail(notFound("symlink", fromPath)) }, truncate(path) { return Effect.fail(notFound("truncate", path)) }, utimes(path) { return Effect.fail(notFound("utimes", path)) }, watch(path) { return Stream.fail(notFound("watch", path)) }, writeFile(path) { return Effect.fail(notFound("writeFile", path)) }, writeFileString(path) { return Effect.fail(notFound("writeFileString", path)) }, ...fileSystem }) /** * Creates a Layer that provides a no-op FileSystem implementation for testing. * * **Details** * * This is a convenience function that wraps `makeNoop` in a Layer, making it easy * to provide the test filesystem to your Effect programs. * * **Example** (Providing a no-op FileSystem layer) * * ```ts import.meta.vitest * import { Effect, FileSystem } from "effect" * * // Create a test layer with specific behaviors * const testLayer = FileSystem.layerNoop({ * readFileString: (path) => Effect.succeed("mocked content"), * exists: () => Effect.succeed(true) * }) * * const program = Effect.gen(function*() { * const fs = yield* FileSystem.FileSystem * const content = yield* fs.readFileString("any-file.txt") * return content * }) * * // Provide the test layer * const testProgram = Effect.provide(program, testLayer) * Effect.runSync(testProgram) // => "mocked content" * ``` * * @stability unstable * @category layers * @since 4.0.0 */ export const layerNoop = (fileSystem: Partial): Layer.Layer => Layer.succeed(FileSystem)(makeNoop(fileSystem)) /** * Runtime type identifier attached to `FileSystem.File` handles and used by * `isFile` to recognize them. * * **Details** * * This marker is part of the runtime representation of file handles. Prefer * `isFile` when narrowing unknown values. * * @see {@link File} for the open file handle shape that carries this marker * @see {@link isFile} for the public guard that checks this marker * * @stability unstable * @category type IDs * @since 4.0.0 */ export const FileTypeId = "~effect/FileSystem/File" /** * Returns `true` if a value is a `File` handle by checking for the * `FileTypeId` marker. * * **When to use** * * Use when accepting an unknown value and you need to narrow it to a `File` * before calling file-handle operations. * * **Details** * * This is a structural marker check. It does not validate the marker value or * the shape of the file handle. * * @see {@link File} for the file-handle interface narrowed by this guard * @see {@link FileTypeId} for the runtime marker checked by this guard * * @stability unstable * @category guards * @since 4.0.0 */ export const isFile = (u: unknown): u is File => hasProperty(u, FileTypeId) /** * Interface representing an open file handle. * * **Details** * * Provides low-level file operations including reading, writing, seeking, * and retrieving file information. File handles are automatically managed * within scoped operations to ensure proper cleanup. * * Each handle tracks its own cursor, which can differ from the POSIX file * offset. In append mode, writes go to the end of the file without moving the * cursor. Outside append mode, `truncate` clamps a cursor past the new length * to that length. * * Positional reads leave the cursor unchanged and may run concurrently on * one handle. Cursor operations (`seek`, reads without a position, and writes) * must not run concurrently with each other. * * **Example** (Working with file handles) * * ```ts import.meta.vitest * import { ByteSize, Effect, FileSystem, Option } from "effect" * * const file: FileSystem.File = { * [FileSystem.FileTypeId]: FileSystem.FileTypeId, * stat: Effect.succeed({ size: ByteSize.bytes(5) } as FileSystem.File.Info), * seek: () => Effect.succeed(BigInt(0)), * sync: Effect.void, * read: (buffer) => Effect.sync(() => { * buffer.set([1, 2, 3, 4, 5]) * return 5 * }), * readAlloc: () => Effect.succeed(Option.none()), * truncate: () => Effect.void, * write: (buffer) => Effect.succeed(buffer.length), * writeAll: () => Effect.void * } * * const program = Effect.gen(function*() { * const stats = yield* file.stat * const buffer = new Uint8Array(5) * const bytesRead = yield* file.read(buffer) * yield* file.writeAll(new TextEncoder().encode("Hello")) * yield* file.sync * return { size: stats.size, bytesRead, buffer: Array.from(buffer) } * }) * * const result = Effect.runSync(program) * ByteSize.toBigInt(result.size) // => 5n * result.bytesRead // => 5 * result.buffer // => [1, 2, 3, 4, 5] * ``` * * @stability unstable * @category models * @since 4.0.0 */ export interface File { readonly [FileTypeId]: typeof FileTypeId /** * Get information about the open file. See `File.Info` for metadata limits. */ readonly stat: Effect.Effect /** * Seeks before the start fail with `BadArgument` and leave the cursor unchanged. */ readonly seek: (offset: bigint, from: SeekMode) => Effect.Effect readonly sync: Effect.Effect /** * Reads into `buffer`, returning the byte count or `0` at EOF. Advances the * cursor unless `options.position` is set (see `File.ReadOptions`). */ readonly read: (buffer: Uint8Array, options?: File.ReadOptions) => Effect.Effect /** * Reads up to `size` bytes into a new buffer, returning `Option.none()` at * EOF. Advances the cursor unless `options.position` is set (see * `File.ReadOptions`). */ readonly readAlloc: ( size: number, options?: File.ReadOptions ) => Effect.Effect, PlatformError> readonly truncate: (length?: number) => Effect.Effect readonly write: (buffer: Uint8Array) => Effect.Effect readonly writeAll: (buffer: Uint8Array) => Effect.Effect } /** * Namespace containing types associated with open file handles, including file * descriptors, entry kinds, and stat information. * * @stability unstable * @since 4.0.0 */ export declare namespace File { /** * Options for `File.read` and `File.readAlloc`. * * **Details** * * `position` is a byte offset from the start of the file. It leaves the * cursor unchanged, fails with `BadArgument` if negative, and reads nothing * at or past EOF. * * **Example** (Reading a byte range without moving the cursor) * * ```ts import.meta.vitest * import type { FileSystem } from "effect" * * const options: FileSystem.File.ReadOptions = { position: BigInt(1024) } * options.position // => 1024n * ``` * * @category models * @since 4.1.0 */ export interface ReadOptions { readonly position?: bigint | undefined } /** * Enumeration of possible file system entry types. * * **Details** * * Represents the different types of entries that can exist in a file system, * from regular files to special device files and symbolic links. * * @category models * @since 4.0.0 */ export type Type = | "File" | "Directory" | "SymbolicLink" | "BlockDevice" | "CharacterDevice" | "FIFO" | "Socket" | "Unknown" /** * Comprehensive file information structure. * * **Details** * * Contains metadata about a file or directory including type, timestamps, * permissions, and size information. This structure is returned by file * stat operations. * * `dev`, `ino`, `rdev`, `nlink`, and `blocks` are `bigint` values, so * 64-bit device identifiers, inode numbers, and counts are not limited to * the safe integer range. `mode`, `uid`, and `gid` are 32-bit values and * remain `number`. Optional metadata is `Option.none()` only when the * platform does not report it. * * Node and Bun report all fields exactly, with `dev`, `ino`, `rdev`, and * `nlink` interpreted as unsigned 64-bit integers. Unsafe `mode` values fail * the stat operation with `BadArgument`. Deno reports these fields as * `number`, so on Deno values above `Number.MAX_SAFE_INTEGER` are already * rounded before they are converted to `bigint`. * * **Example** (Inspecting file information) * * ```ts import.meta.vitest * import { ByteSize, FileSystem, Option } from "effect" * * const info: FileSystem.File.Info = { * type: "File", * mtime: Option.none(), * atime: Option.none(), * birthtime: Option.none(), * dev: 1n, * ino: Option.none(), * mode: 0o644, * nlink: Option.none(), * uid: Option.none(), * gid: Option.none(), * rdev: Option.none(), * size: ByteSize.bytes(5), * blksize: Option.none(), * blocks: Option.none() * } * * info.type // => "File" * ByteSize.toBigInt(info.size) // => 5n * info.mode.toString(8) // => "644" * * const modified = Option.match(info.mtime, { * onNone: () => "unavailable", * onSome: (mtime) => mtime.toISOString() * }) * modified // => "unavailable" * info.type === "File" // => true * ``` * * @category models * @since 4.0.0 */ export interface Info { readonly type: Type readonly mtime: Option.Option readonly atime: Option.Option readonly birthtime: Option.Option readonly dev: bigint readonly ino: Option.Option readonly mode: number readonly nlink: Option.Option readonly uid: Option.Option readonly gid: Option.Option readonly rdev: Option.Option readonly size: ByteSize.ByteSize readonly blksize: Option.Option readonly blocks: Option.Option } } /** * Specifies the reference point for seeking within an open file. * * **When to use** * * Use with `File` handles when positioning the cursor before a read or write * and the offset must be interpreted from either the start of the file or the * current cursor. * * **Details** * * - `"start"` seeks from the beginning of the file. * - `"current"` seeks from the current cursor position. * * @see {@link File} for the open file handle API whose `seek` method consumes this mode * * @stability unstable * @category models * @since 4.0.0 */ export type SeekMode = "start" | "current" /** * Options for watching files or directories. * * @stability unstable * @category models * @since 4.0.0 */ export interface WatchOptions { /** * When `true`, changes in subdirectories are also reported. */ readonly recursive?: boolean | undefined /** * Called once the underlying watcher is installed and can buffer events. * Stream construction alone does not install a watcher. Custom watch * backends must also call this callback after installation, when supplied. */ readonly onReady?: (() => void) | undefined } /** * Represents file system events emitted when watching files or directories. * * **When to use** * * Use when consuming file system watch streams and pattern matching on `_tag` * to handle created, updated, or removed paths. * * **Details** * * The union covers create, update, and remove events. Each event carries the * reported `path`. * * @see {@link FileSystem} for the service interface whose `watch` operation emits these events * * @stability unstable * @category models * @since 4.0.0 */ export type WatchEvent = WatchEvent.Create | WatchEvent.Update | WatchEvent.Remove /** * Namespace containing the concrete event shapes emitted by `FileSystem.watch`. * * @stability unstable * @since 4.0.0 */ export declare namespace WatchEvent { /** * Event representing the creation of a new file or directory. * * **Details** * * This event is triggered when a new file or directory is created * in the watched location. * * @category models * @since 4.0.0 */ export interface Create { readonly _tag: "Create" readonly path: string } /** * Event representing the modification of an existing file or directory. * * **Details** * * This event is triggered when an existing file or directory is * modified in the watched location. * * @category models * @since 4.0.0 */ export interface Update { readonly _tag: "Update" readonly path: string } /** * Event representing the deletion of a file or directory. * * **Details** * * This event is triggered when a file or directory is deleted * from the watched location. * * @category models * @since 4.0.0 */ export interface Remove { readonly _tag: "Remove" readonly path: string } } /** * Service key for file system watch backend implementations. * * **Details** * * This service provides the low-level file watching capabilities that can be * implemented differently on various platforms (e.g., inotify on Linux, * FSEvents on macOS, etc.). * * **Example** (Providing a custom watch backend) * * ```ts import.meta.vitest * import { Effect, FileSystem, Option, Stream } from "effect" * * // Custom watch backend implementation * const customWatchBackend = { * register: (path: string, stat: FileSystem.File.Info) => { * // Implementation would depend on platform * return Option.some(Stream.empty) // Placeholder implementation * } * } * * const program = Effect.gen(function*() { * const backend = yield* FileSystem.WatchBackend * return Option.isSome( * backend.register("./directory", { type: "Directory" } as FileSystem.File.Info) * ) * }) * * const withCustomBackend = Effect.provideService( * program, * FileSystem.WatchBackend, * customWatchBackend * ) * Effect.runSync(withCustomBackend) // => true * ``` * * @stability unstable * @category services * @since 4.0.0 */ export class WatchBackend extends Context.Service Option.Option> }>()("effect/FileSystem/WatchBackend") {}