char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somadd makeTemp{Dir,File} apis to avril:fs791f952

main
5.3 KiB131 linesraw

Filesystem I/O

Filesystem APIs come in asynchronous and synchronous modules:

AsyncSync
Primitivesavril:stdavril:std/sync
Convenience operationsavril:fsavril:fs/sync

The sync modules use the same function names, without a Sync suffix. They return values directly and throw errors synchronously. No CLI flag is needed.

import { readTextFile } from "avril:fs/sync";

const config = JSON.parse(readTextFile("config.json"));

When mixing modules, alias imports as needed:

import { readTextFile } from "avril:fs";
import { readTextFile as readTextFileSync } from "avril:fs/sync";

For lexical path manipulation without filesystem access (joining, normalization, parsing, file: URL conversion), see avril:path.

Synchronous primitives

avril:std/sync exports:

  • open, create, stat, lstat, readDir
  • mkdir, remove, rename, copyFile
  • symlink, readLink, realPath, link
  • chmod, chown, lchown, utime, lutime, truncate
  • The same FileHandle, SeekMode, and errors objects as avril:std.

Options, paths (strings or file URLs), metadata, errors, and platform limitations match the async APIs. readDir returns a synchronous iterable; use for ... of. mkdir creates one directory; use mkdirs for parents. Watchers remain in avril:std and are asynchronous.

Handles

Both versions of open and create return the same FileHandle, with async methods and synchronous counterparts:

  • readSync, writeSync, seekSync, statSync, truncateSync
  • syncSync, syncDataSync
  • fchmodSync, fchownSync, futimeSync, flockSync, funlockSync

close() and [Symbol.dispose]() are already synchronous. A handle retains its readable/writable Web Streams and can be lent to subprocesses regardless of how it was opened.

import { open, SeekMode } from "avril:std/sync";

const file = open("data.bin", { read: true, write: true });
try {
  const header = new Uint8Array(16);
  const count = file.readSync(header);
  file.seekSync(0, SeekMode.Start);
  await file.write(new Uint8Array([1, 2, 3]));
} finally {
  file.close();
}

Direct reads and writes may be partial. Reads return null at EOF, or 0 for an empty buffer. Use the convenience functions for whole-file transfers. Opening non-regular files is not supported.

writev (and writevSync) writes an array of views as one ordered operation: the byte sequence equals the concatenation of its views, empty views are skipped, and unlike write the call completes the entire vector before resolving with the total bytes written. No other operation on the handle can interleave between its batches, and an error may leave a prefix written without rolling it back.

The whole vector is validated and snapshotted at invocation, so mutating the array or its views afterwards cannot affect any submitted bytes. An empty or all-empty vector returns 0, still performing the usual closed and authority checks. Snapshots retain the entire payload until completion; await writes rather than queueing unbounded batches.

Sync and async handle operations share the native queue and file cursor. A sync operation waits behind already-submitted native operations, but does not run JavaScript callbacks or settle their JS promises while waiting. Web Stream writes can defer submission until a later JS turn; await stream writes before mixing them with direct handle operations.

Advisory lock waits release the handle queue, as with async flock. Locks are exclusive by default. A synchronous lock wait blocks its isolate: a timer or promise callback in that same isolate cannot release the conflicting lock. Another isolate or process must do so. Terminating a worker interrupts its synchronous lock wait.

Convenience operations

avril:fs/sync exports readFile, readTextFile, writeFile, writeTextFile, mkdirs, makeTempDir, makeTempFile, copy, move, and exists.

These follow the async module's behavior:

  • Whole-file operations handle partial reads and writes and close their handles.
  • Text is UTF-8. Writes support append, create, createNew, and mode.
  • mkdirs creates missing parents and accepts existing directories.
  • copy handles trees and preserves symlinks by default; { dereference: true } follows them and rejects ancestor cycles. Ordinary mode bits are preserved.
  • move tries rename, then copies and removes on cross-device moves. That fallback is not atomic.
  • exists returns false only for missing paths; permission errors still throw.
  • makeTempDir and makeTempFile create a uniquely-named empty directory (0o700) or file (0o600) from { dir, prefix, suffix }, defaulting to the OS temp directory and empty affixes. Names are prefix plus random hex plus suffix; collisions retry. Missing parents are an error, and the caller removes the result. Prefixes and suffixes reject control characters and <>:"/\\|?*.

Authority and scope

Both modes use the same capability checks, including live pledge() restrictions on already-open handles and symlink-target checks. The sync modules do not grant additional authority.

These are avril filesystem APIs, not a Deno facade. Blocking standard-stream interfaces are also available from avril:std/sync; see standard I/O. There are no synchronous socket APIs or globalThis.Deno.

1# Filesystem I/O
2
3Filesystem APIs come in asynchronous and synchronous modules:
4
5| | Async | Sync |
6|---|---|---|
7| Primitives | `avril:std` | `avril:std/sync` |
8| Convenience operations | `avril:fs` | `avril:fs/sync` |
9
10The sync modules use the same function names, without a `Sync` suffix. They
11return values directly and throw errors synchronously. No CLI flag is needed.
12
13```ts
14import { readTextFile } from "avril:fs/sync";
15
16const config = JSON.parse(readTextFile("config.json"));
17```
18
19When mixing modules, alias imports as needed:
20
21```ts
22import { readTextFile } from "avril:fs";
23import { readTextFile as readTextFileSync } from "avril:fs/sync";
24```
25
26For lexical path manipulation without filesystem access (joining,
27normalization, parsing, `file:` URL conversion), see [`avril:path`](path.md).
28
29## Synchronous primitives
30
31`avril:std/sync` exports:
32
33- `open`, `create`, `stat`, `lstat`, `readDir`
34- `mkdir`, `remove`, `rename`, `copyFile`
35- `symlink`, `readLink`, `realPath`, `link`
36- `chmod`, `chown`, `lchown`, `utime`, `lutime`, `truncate`
37- The same `FileHandle`, `SeekMode`, and `errors` objects as `avril:std`.
38
39Options, paths (strings or file URLs), metadata, errors, and platform
40limitations match the async APIs. `readDir` returns a synchronous iterable;
41use `for ... of`. `mkdir` creates one directory; use `mkdirs` for parents.
42Watchers remain in `avril:std` and are asynchronous.
43
44## Handles
45
46Both versions of `open` and `create` return the same `FileHandle`, with async
47methods and synchronous counterparts:
48
49- `readSync`, `writeSync`, `seekSync`, `statSync`, `truncateSync`
50- `syncSync`, `syncDataSync`
51- `fchmodSync`, `fchownSync`, `futimeSync`, `flockSync`, `funlockSync`
52
53`close()` and `[Symbol.dispose]()` are already synchronous. A handle retains
54its readable/writable Web Streams and can be lent to subprocesses regardless
55of how it was opened.
56
57```ts
58import { open, SeekMode } from "avril:std/sync";
59
60const file = open("data.bin", { read: true, write: true });
61try {
62  const header = new Uint8Array(16);
63  const count = file.readSync(header);
64  file.seekSync(0, SeekMode.Start);
65  await file.write(new Uint8Array([1, 2, 3]));
66} finally {
67  file.close();
68}
69```
70
71Direct reads and writes may be partial. Reads return `null` at EOF, or `0`
72for an empty buffer. Use the convenience functions for whole-file transfers.
73Opening non-regular files is not supported.
74
75`writev` (and `writevSync`) writes an array of views as one ordered operation:
76the byte sequence equals the concatenation of its views, empty views are
77skipped, and unlike `write` the call completes the entire vector before
78resolving with the total bytes written. No other operation on the handle can
79interleave between its batches, and an error may leave a prefix written
80without rolling it back.
81
82The whole vector is validated and snapshotted at invocation, so mutating the
83array or its views afterwards cannot affect any submitted bytes. An empty or
84all-empty vector returns 0, still performing the usual closed and authority
85checks. Snapshots retain the entire payload until completion; await writes
86rather than queueing unbounded batches.
87
88Sync and async handle operations share the native queue and file cursor. A
89sync operation waits behind already-submitted native operations, but does not
90run JavaScript callbacks or settle their JS promises while waiting. Web Stream
91writes can defer submission until a later JS turn; await stream writes before
92mixing them with direct handle operations.
93
94Advisory lock waits release the handle queue, as with async `flock`. Locks are
95exclusive by default. A synchronous lock wait blocks its isolate: a timer or
96promise callback in that same isolate cannot release the conflicting lock.
97Another isolate or process must do so. Terminating a worker interrupts its
98synchronous lock wait.
99
100## Convenience operations
101
102`avril:fs/sync` exports `readFile`, `readTextFile`, `writeFile`,
103`writeTextFile`, `mkdirs`, `makeTempDir`, `makeTempFile`, `copy`, `move`,
104and `exists`.
105
106These follow the async module's behavior:
107
108- Whole-file operations handle partial reads and writes and close their handles.
109- Text is UTF-8. Writes support `append`, `create`, `createNew`, and `mode`.
110- `mkdirs` creates missing parents and accepts existing directories.
111- `copy` handles trees and preserves symlinks by default; `{ dereference: true }`
112  follows them and rejects ancestor cycles. Ordinary mode bits are preserved.
113- `move` tries rename, then copies and removes on cross-device moves. That
114  fallback is not atomic.
115- `exists` returns false only for missing paths; permission errors still throw.
116- `makeTempDir` and `makeTempFile` create a uniquely-named empty directory
117  (`0o700`) or file (`0o600`) from `{ dir, prefix, suffix }`, defaulting to
118  the OS temp directory and empty affixes. Names are `prefix` plus random hex
119  plus `suffix`; collisions retry. Missing parents are an error, and the
120  caller removes the result. Prefixes and suffixes reject control characters
121  and `<>:"/\\|?*`.
122
123## Authority and scope
124
125Both modes use the same capability checks, including live `pledge()`
126restrictions on already-open handles and symlink-target checks. The sync
127modules do not grant additional authority.
128
129These are avril filesystem APIs, not a Deno facade. Blocking standard-stream
130interfaces are also available from `avril:std/sync`; see [standard I/O](stdio.md).
131There are no synchronous socket APIs or `globalThis.Deno`.