# Files

A space has one folder tree. A file grant is a permission entry with the same actions as a table grant,
and `access.request` is the call that asks for it. A missing name and a hidden name are both not found.

## A tiny example

```python
from adoor import Client

db = Client("https://app.adoor.run", token)
space = "…"
db.fs_write(space, body="hello\n", path="", name="note.txt")
print(db.fs_read(space, path="note.txt")["body"])
```

That writes one file of a few bytes and reads it back. `token` is a connection key,
the same key the HTTP API sends as `Authorization: Bearer`.
A key without a file grant for that path gets `NOT_FOUND`.

## Quickstart

Install and sign in the way the [Python](/docs/guides/python/) guide describes.
The calls below use that client. MCP and `POST /api/operations/<name>` take the same inputs.

`fs_stat` reads one name. `fs_list` reads one page of a directory. Neither writes an event.

```python
db.fs_stat(space, path="note.txt")
db.fs_list(space, path="")
```

`fs_mkdir` creates a directory. `fs_write` creates or replaces a file of at most 32 KiB.
Pass the generation from stat when you replace. `fs_read` returns the text when the bytes are UTF-8.

```python
folder = db.fs_mkdir(space, name="lab")
note = db.fs_write(space, body="plate P1\n", path="lab", name="read.txt")
db.fs_read(space, path="lab/read.txt")["body"]
```

`fs_move` reparents a file. `fs_remove` deletes a file or an empty directory. Both take the generation from stat. A directory that still has children stays.

```python
db.fs_move(note["id"], space, folder["id"], note["generation"])
db.fs_remove(space, inode_id=note["id"], expected_generation=note["generation"])
```

History and undo are the table calls. Pass the inode to read one file. Undo reverses one of your own changes.

```python
events = db.history(inode_id=note["id"], space=space)
db.undo(events[0]["id"])
```

`fs_diff` compares two content ids, or two event ids, of one file.

A grant names `resource`, `inodeId`, and `actions`. `inodeId` may be `*` for the whole space.
The actions are `select`, `insert`, `update`, `delete`, `alter`, and `drop_table`.
Ask with `access.request`. That is the same call a table grant uses.

```python
db.call("access.request", {
    "permissions": [{"resource": "file", "space": space, "inodeId": folder["id"], "actions": ["select", "update"]}],
    "reason": "Read and replace the lab notes.",
})
```

The call stores a request. It does not grant anything until a person approves it.

## Tutorial

This walk keeps a lab note, reads who changed it, and puts it back.

Create the directory and the note.

```python
folder = db.fs_mkdir(space, name="run-1")
note = db.fs_write(space, body="od600 0.42\n", path="run-1", name="curve.txt")
```

List the directory. The page is the names you can select. A name outside the grant is absent, not listed as forbidden.

```python
page = db.fs_list(space, inode_id=folder["id"])
```

Replace the note. The generation from the write is the one stat would return.

```python
replaced = db.fs_write(
    space, body="od600 0.48\n", inode_id=note["id"], expected_generation=note["generation"]
)
```

Read the file history. The newest event is the replacement. Undo it. The body is the first write again.

```python
events = db.history(inode_id=note["id"], space=space)
db.undo(events[0]["id"])
db.fs_read(space, inode_id=note["id"])["body"]
```

Diff those two versions by their event ids. Text of at most 1 MiB on both sides comes back as a unified diff.

```python
db.fs_diff(space, inode_id=note["id"], left_event_id=events[1]["id"], right_event_id=events[0]["id"])
```

Ask for a narrower key for a notebook that should only read this directory. The entry is a file grant. The call is the table grant call.

```python
db.call("access.request", {
    "permissions": [{"resource": "file", "space": space, "inodeId": folder["id"], "actions": ["select"]}],
    "reason": "The notebook only reads this run.",
})
```

A key that has no file grant for `curve.txt` gets `NOT_FOUND` from `fs_read`, the same way a key with no table grant gets `NOT_FOUND` for a hidden table.

Delete the note when the run is done. An empty directory can be removed the same way.

```python
current = db.fs_stat(space, inode_id=note["id"])
db.fs_remove(space, inode_id=note["id"], expected_generation=current["generation"])
```
