# `Apero.File`
[🔗](https://github.com/Lorenzo-SF/apero/blob/3.0.0/lib/apero/file.ex#L1)

File system utilities — file ops, path operations, atomic writes, temp resources, locking.

Provides domain-specific tools for file and filesystem operations behind a
consistent `{:ok, result} | {:error, reason}` interface.

## Submodules

  * `Apero.File.IO` — I/O operations (atomic writes, checksums, temp resources, locking)
  * `Apero.File.Path` — path operations (copy, move, delete, glob, etc.)
  * `Apero.File.Tree` — ASCII tree generation and printing
  * `Apero.File.Watcher` — file system watching via GenServer

## Watching

File watching moved to `Trebejo.File.watch/3` in v3.0.0 because it
depends on `Arrea.WorkerSupervisor`. The `Apero.File.Watcher` GenServer
remains in Apero (it is pure OTP), but the `Trebejo.File.watch/3` convenience wrapper
is now in Trebejo.

## Backward Compatibility

`Apero.VFS` is an alias for `Apero.File` for backward compatibility.

## Security note

`atomic_write/2` always creates the temp file in the same directory as the
target to guarantee `rename(2)` atomicity (cross-device renames fall back
to copy+delete).

# `atomic_write`

```elixir
@spec atomic_write(binary(), iodata()) :: :ok | {:error, binary()}
```

Writes `content` to `path` atomically using a temp-file + rename.

The temp file is created in the same directory as `path` to ensure the
rename is atomic on POSIX systems. Parent directories are created as needed.

## Examples

    iex> path = Path.join(System.tmp_dir!(), "apero_atomic_test.txt")
    iex> :ok = Apero.File.atomic_write(path, "hello")
    iex> File.read!(path)
    "hello"
    iex> File.rm!(path)
    :ok

# `basename`

```elixir
@spec basename(binary()) :: binary()
```

Returns the filename without extension.

## Examples

    iex> Apero.File.basename("path/to/file.ex")
    "file"

# `checksum`

```elixir
@spec checksum(binary(), :sha256 | :sha512 | :md5 | :sha) ::
  {:ok, binary()} | {:error, binary()}
```

Computes the checksum of a file by streaming its contents in 64KB chunks.
Never loads the entire file into memory.

Supported algorithms: `:sha256`, `:sha512`, `:md5`, `:sha`.
Returns the lowercase hex-encoded digest.

## Examples

    iex> path = Path.join(System.tmp_dir!(), "apero_cs_test.bin")
    iex> File.write!(path, "hello")
    iex> {:ok, digest} = Apero.File.checksum(path, :sha256)
    iex> String.length(digest)
    64
    iex> File.rm!(path)
    :ok

# `checksum_many`

```elixir
@spec checksum_many([binary()], :sha256 | :sha512 | :md5 | :sha) :: %{
  required(binary()) =&gt; {:ok, binary()} | {:error, binary()}
}
```

Computes checksums for multiple files in parallel.

Returns a map of `path => {:ok, digest} | {:error, reason}`.

# `copy`

```elixir
@spec copy(binary(), binary()) :: {:ok, non_neg_integer()} | {:error, binary()}
```

Copies `source` to `dest`. Creates parent directories as needed.
Returns `{:ok, bytes_copied}`.

# `copy_dir`

```elixir
@spec copy_dir(binary(), binary()) :: :ok | {:error, binary()}
```

Recursively copies a directory from `source` to `dest`.

# `copy_many`

```elixir
@spec copy_many([{binary(), binary()}]) :: [ok: non_neg_integer(), error: binary()]
```

Copies multiple files in parallel.

Each element of `pairs` is a `{source, dest}` tuple.
Returns `[{:ok, bytes} | {:error, reason}]` in the same order.

# `delete`

```elixir
@spec delete(binary()) :: :ok | {:error, binary()}
```

Deletes a file. Returns `:ok` even if the file did not exist.

# `delete_dir`

```elixir
@spec delete_dir(binary()) :: :ok | {:error, binary()}
```

Recursively deletes a directory and all its contents.

# `dir?`

```elixir
@spec dir?(binary()) :: boolean()
```

Returns `true` if the given path is an existing directory.

# `ensure_dir`

```elixir
@spec ensure_dir(binary()) :: :ok | {:error, File.posix()}
```

Creates the directory and all parents. Idempotent.

# `exists?`

```elixir
@spec exists?(binary()) :: boolean()
```

Returns `true` if the given path exists (file, directory or symlink).

# `expand`

```elixir
@spec expand(binary()) :: binary()
```

Expands `~` and relative path segments to an absolute path.

# `extension`

```elixir
@spec extension(binary()) :: binary()
```

Returns the extension of a file path.

## Examples

    iex> Apero.File.extension("archive.tar.gz")
    ".gz"

# `file?`

```elixir
@spec file?(binary()) :: boolean()
```

Returns `true` if the given path is an existing regular file.

# `generate_tree`

```elixir
@spec generate_tree([binary()]) :: binary()
```

Generates a visual ASCII tree from a list of file paths.

Returns a printable string with `├─` and `└─` connectors.
The last item at each level uses `└─`, all others use `├─`.

## Examples

    iex> Apero.File.generate_tree(["a"])
    "└─ a"

# `glob`

```elixir
@spec glob(binary(), binary(), keyword()) :: [binary()]
```

Lists files matching a glob pattern in `dir`.

## Options

  * `:recursive` — recurse into subdirectories (default: `false`)

# `move`

```elixir
@spec move(binary(), binary()) :: :ok | {:error, binary()}
```

Moves `source` to `dest`. Uses rename when possible, falls back to copy+delete
on cross-device moves.

# `mtime`

```elixir
@spec mtime(binary()) :: {:ok, NaiveDateTime.t()} | {:error, File.posix()}
```

Returns the last modification time of a file as a `NaiveDateTime`.

# `print_tree`

```elixir
@spec print_tree(binary()) :: :ok
```

Prints an ASCII directory tree of `path` to stdout.

# `read`

```elixir
@spec read(binary()) :: {:ok, binary()} | {:error, binary()}
```

Reads a text file and returns its contents.

# `read_lines`

```elixir
@spec read_lines(binary()) :: {:ok, [binary()]} | {:error, binary()}
```

Reads all non-empty, non-comment lines from a file.

Lines starting with `#` and blank lines are removed. Content is trimmed.

# `size`

```elixir
@spec size(binary()) :: {:ok, non_neg_integer()} | {:error, File.posix()}
```

Returns the file size in bytes.

# `symlink`

```elixir
@spec symlink(binary(), binary()) :: :ok | {:error, binary()}
```

Creates a symbolic link at `link_path` pointing to `target`.

# `with_lock`

```elixir
@spec with_lock(binary(), keyword(), (-&gt; any())) :: any() | {:error, :timeout}
```

Acquires an advisory lock on `lock_path`, runs `fun`, then releases it.

Retries on `EEXIST` until timeout. Returns `{:error, :timeout}` if the
lock cannot be acquired within the deadline.

## Options

  * `:timeout_ms` — how long to wait in ms (default: `5_000`)
  * `:retry_ms` — poll interval in ms (default: `100`)

# `with_tmp_dir`

```elixir
@spec with_tmp_dir(
  keyword(),
  (binary() -&gt; any())
) :: any()
```

Creates a temporary directory, yields its path to `fun`, then deletes it.
The directory is deleted even when `fun` raises.

# `with_tmp_file`

```elixir
@spec with_tmp_file(
  keyword(),
  (binary() -&gt; any())
) :: any()
```

Creates a temporary file, yields its path to `fun`, then deletes it.
The file is deleted even when `fun` raises.

## Options

  * `:suffix` — file extension suffix (default: `""`)
  * `:dir` — parent directory (default: `System.tmp_dir!/0`)

# `write`

```elixir
@spec write(binary(), iodata()) :: :ok | {:error, binary()}
```

Writes text to a file, creating parent directories as needed.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
