specs/052-file.md
Rama main · réplica de lectura del repositorio original.
# Spec: File Actions
## Status
Partially implemented.
Conformance covers one local workflow using each action and required-field
validation. Executor tests own permission modes, overwrite and dry-run
options, recursive traversal, metadata details, and filesystem edge cases.
This spec defines conformance behavior for the built-in `file.stat`,
`file.read`, `file.write`, `file.copy`, `file.move`, `file.delete`,
`file.mkdir`, and `file.list` actions.
## Scope
This spec defines the `file.*` actions, which operate on ordinary local
filesystem paths relative to the step working directory or an absolute path.
This spec covers:
- `with.path`'s resolution: relative to the step's working directory
when relative, used as-is (after `~` expansion) when absolute -- there
is no artifact-style restriction to a sandboxed directory
- `file.write`'s required `with.content`, `with.mode`, `with.overwrite`
(default `false`: writing to an existing path fails), `with.atomic`,
`with.create_dirs`, and `with.dry_run`
- `file.read`'s required `with.path`, `with.format` (`raw`, the default,
streaming the file's exact bytes; or `json`, wrapping the content with
metadata), and `with.max_bytes`
- `file.stat`'s `with.missing_ok` (succeed, reporting `exists: false`,
instead of failing when the path is absent)
- `file.copy` and `file.move`'s required `with.source` and
`with.destination`, `with.recursive` (required to copy a directory),
and that a copy destination inside the source is rejected
- `file.move`'s default requirement that the destination not already
exist, and that the source no longer exists afterward
- `file.delete`'s `with.recursive` (required to delete a directory) and
`with.missing_ok`
- `file.list`'s `with.pattern` (a glob), `with.recursive` (default
`false`: only direct children), and `with.include_dirs` (default
`false`: files only)
- that every configuration error this spec documents is rejected at
DAG-build-time validation, not only at runtime
- validation and runtime errors
This spec does not define:
- `with.follow_symlinks`, or symlink handling generally, beyond what a
reader needs to know that it exists
- directory moves and cross-filesystem move behavior
- refusing to delete the filesystem root, beyond noting that the
behavior exists (this spec does not exercise it directly, since doing
so live would require actually attempting that deletion)
- direct `type: file` authoring
## Goal
Workflow authors manage ordinary files on the local filesystem as DAG
steps -- reading, writing, copying, moving, deleting, creating
directories, and listing -- without shelling out to a command executor
for what is otherwise a small, declarative file operation.
## Behavior
### Path resolution
Every path field (`with.path`, `with.source`, `with.destination`) is
resolved the same way: relative to the step's working directory when it
is relative, or used directly (after expanding a leading `~`) when it is
absolute. Unlike `artifact.*`, there is no restriction keeping a path
inside any particular directory.
### Write
`file.write` requires `with.content` and `with.path`. `with.mode` sets
the new file's permission bits (an octal string such as `"0640"`);
it defaults to `0600`. The process umask may restrict permissions on
new files. Without `with.overwrite`, writing to a path that
already exists fails; with `with.overwrite: true`, the existing file is
replaced (`with.atomic`, default `true`, governs how). `with.create_dirs:
true` creates missing parent directories first. `with.dry_run: true`
reports what would happen (`created: false`) without writing anything.
### Read and stat
`file.read` requires `with.path`. `with.format: raw` (the default)
copies the file's exact bytes to stdout; `with.format: json` instead
writes one JSON object with the file's path, type, size, mode,
modification time, and content. `with.max_bytes`, when greater than
zero, rejects a file larger than that many bytes. Reading a directory
fails.
`file.stat` reports the same metadata (without content) for any path.
`with.missing_ok: true` makes a missing path succeed, with `exists:
false`, instead of failing.
### Copy and move
`file.copy` and `file.move` require `with.source` and
`with.destination`; the two must differ. Copying a directory requires
`with.recursive: true`; a copy destination inside the source is rejected.
`file.move`, without `with.overwrite`, fails if the destination
already exists; on success, the source no longer exists.
`with.create_dirs: true` creates missing destination parent directories
first. `with.dry_run: true` reports the intended operation without
performing it.
### Delete and mkdir
`file.delete` requires `with.path`. Deleting a directory requires
`with.recursive: true`. `with.missing_ok: true` makes a missing path
succeed, with `deleted: false`, instead of failing. `file.mkdir` creates
`with.path` (and any missing parents), applying `with.mode` when set,
subject to the process umask.
### List
`file.list` requires `with.path` (the directory to list) and writes one
JSON object naming the count of files found and an array of entries
(each with path, type, size, mode, and modification time), sorted by
path. `with.recursive: true` descends into subdirectories; otherwise
only direct children are listed. `with.include_dirs: true` includes
directory entries; otherwise only regular files are listed.
`with.pattern`, when set, is a glob matched against each candidate's
slash-separated path relative to `with.path`.
### Dry run
`dagu dry` (Spec 064) warns, without failing, when a file the step reads is
not on this host. `file.read` and `file.list` need `with.path`.
`file.stat` and `file.delete` need `with.path` unless `with.missing_ok` is
set. `file.copy` and `file.move` need `with.source`, unless `with.dry_run`
is set. A path that still holds a `$` reference once params and environment
resolve, such as an earlier step's output variable, is skipped. `file.write`
and `file.mkdir` create their path, so a missing destination is not a warning.
## Errors
### Validation
Every one of these is rejected at DAG-build-time validation (`dagu
validate`), not only when the step runs:
- `file.write` without `with.content`.
- `file.stat`/`file.read`/`file.delete`/`file.mkdir`/`file.list` without
`with.path`.
- `file.copy`/`file.move` without `with.source` or `with.destination`.
### Runtime
- `file.write` to an existing path without `with.overwrite`: an error
containing `"file exists"`.
- `file.read` on a file larger than `with.max_bytes`: an error
containing `"exceeds max_bytes"`.
- `file.read` on a directory: an error containing `"cannot read a
directory"`.
- `file.copy` on a directory without `with.recursive`: an error
containing `"recursive is required to copy a directory"`.
- `file.copy` with a destination inside the source: an error containing
`"destination must not be inside source"`.
- `file.move` to an existing destination without `with.overwrite`: an
error containing `"destination exists"`.
- `file.delete` on a directory without `with.recursive`: an error
containing `"recursive is required to delete a directory"`.
## Related Specs
- Run context: [Spec 017: Built-In Run Context](017-built-in-run-context.md)
- Dry-run step checks: [Spec 064: Dry-Run Step Checks](064-dry-run-step-checks.md)
## Examples
Copy a directory tree, creating the destination's parents if needed:
```yaml
steps:
- action: file.copy
with:
source: ./build/output
destination: /srv/releases/latest
recursive: true
create_dirs: true
overwrite: true
```
Check whether a file exists before deciding what to do next:
```yaml
steps:
- id: check
action: file.stat
with:
path: ./cache/result.json
missing_ok: true
```