directory

← All adapters

directory(options: DirectoryOptions & { chunked: true }): Source<DirectoryEntry>
directory(options: DirectoryOptions): Source<DirectoryEntry[]>

Scan a directory and list its entries. The directory adapter is the "find the files" half of working with a directory; the file adapter reads or writes a single file. Compose the two to process every file in a directory.

By default the source emits a single exchange whose body is the full DirectoryEntry[] listing, the same collection-in-one-exchange shape as the non-chunked csv and jsonl adapters. Pass chunked: true to emit one exchange per entry instead. Filtering is intentionally not built in either way: list the entries, then decide which ones.

// Chunked: one exchange per file, filter by name/metadata, read each
craft()
  .from(directory({ path: './inbox', chunked: true }))
  .filter((ex) => ex.body.ext === '.json')
  .enrich(
    file({ path: (ex) => ex.body.path, mode: 'read' }),
    only((content: string) => content, 'content'),
  )
  .to(log())

// Default: the whole listing as one body, act on the collection then split
craft()
  .from(directory({ path: './inbox' }))
  .transform((entries) => entries.filter((e) => e.ext === '.json'))
  .split((ex) => ex.body)
  .enrich(
    file({ path: (ex) => ex.body.path, mode: 'read' }),
    only((content: string) => content, 'content'),
  )
  .to(log())

Options:

OptionTypeDefaultDescription
pathstringRequiredDirectory to scan (a source scans one directory, so no dynamic function form)
recursivebooleanfalseDescend into subdirectories
includeDirsbooleanfalseEmit directory entries too, not just files
chunkedbooleanfalseEmit one exchange per entry instead of a single DirectoryEntry[] exchange

Entry shape (DirectoryEntry): the body of every emitted exchange.

FieldTypeDescription
pathstringPath resolved against the scanned directory, ready for file({ path })
namestringBase name including extension, e.g. report.json
dirstringDirectory containing the entry
extstringLowercased extension including the dot, e.g. .json (empty when none)
relativePathstringPath relative to the scanned directory root
sizenumberFile size in bytes
modifiedAtDateLast modification time
createdAtDateCreation time (birthtime; may fall back to the mtime on some filesystems)
isDirectorybooleanTrue for directory entries (only emitted when includeDirs)

Metadata lives on the body, not headers: the entry is already a structured object, so its fields are not duplicated into routecraft.directory.* headers. Filter and route on the body directly. This differs from the file adapter's chunked mode, whose body is a bare line string and so carries its line number and path on headers.

Deterministic order: entries are sorted by relativePath with separators normalized to /, so emission order (chunked) and array order (non-chunked) are stable and identical across platforms (raw directory listing order is not). An empty directory emits one exchange with an empty array in the default shape, and nothing in chunked mode.

Files only by default: directories are skipped unless includeDirs: true. With recursive: true the scan still descends into subdirectories regardless of includeDirs; that flag only controls whether the directories themselves are emitted as exchanges.

Symlinks are followed: entry types and metadata come from the target, not the link. A symlink to a directory is treated as a directory (skipped unless includeDirs), a symlink to a file is emitted with the target's size and modifiedAt, and a broken symlink is skipped.

Robust scanning: an entry that vanishes between listing and reading its metadata (or a broken symlink) is skipped with a debug log rather than failing the whole scan; any other per-entry failure (for example an unreadable entry) is also skipped but logged as a warning, since the listing is incomplete. A missing or unreadable directory throws (directory not found, not a directory, or permission denied).

Exported symbols: directory; types DirectoryAdapter, DirectoryOptions, DirectoryEntry