Utility: move - relocate a file

Status: Built + QEMU-verified (osdev test files, 222/0). Same-drive move is a relink - only the directory entries change, no data copied and (so) no reclamation needed; fs treats a same-directory move as a rename. (Cross-drive move = copy + delete is later, with multi-drive.) Trails CLAUDE.md; does not amend it.


1. What it is

move <src> <dst> relocates a file to a new path - a different directory, or a different drive (move 0:data/a 1:archive/a). It replaces POSIX mv, but only the relocation half: renaming a file in place is a separate verb, rename (23_rename.md). POSIX's mv secretly does both; GodspeedOS keeps them distinct because they are different acts (§26.5).

2. Usage

move 0.4.0 - relocate a file or directory

usage:
  move <src> <dst>    move <src> to <dst>
  move version        print the version
  move help           print this message

<src>,<dst> = [index:]label/path | /abs | rel   (see docs/drives.md §4.1)

3. Behaviour

Built and shipping. This section used to read "why it's gated" and explained that move was deliberately withheld because GSFS had no block reclamation, so a move would strand the source's blocks. Reclamation landed (docs/persistence.md §6.11) and move shipped with it; the section described a state that had not been true for some time.

A same-directory move is a rename (re-point the entry). Across directories it is an dir_add into the destination followed by a dir_remove from the source, both inside one journal transaction, so a crash leaves the file in exactly one of the two places and never in neither or both. Moving a directory moves its whole subtree with it; nothing is copied, because nothing needs to be.

Open capabilities to the moved path are revoked (revoke_open_subtree), including for every descendant when a directory moves. A capability names a path, the path no longer names that file, and a holder that kept using it would be a confused deputy (§7.10, SEC-5).

A directory cannot be moved into itself, or into its own subtree

move /a /a/b would write an entry inside /a pointing at /a, then unlink /a from its parent. The subtree becomes a cycle that the root cannot reach - and drives check rebuilds the free bitmap by walking the tree, so those still-occupied blocks would be marked free and handed to the next allocation, which overwrites live data. MAX_TREE_DEPTH keeps a walk from hanging on it, so it would show up as a leak that turns into corruption rather than as a hang.

It is refused in two places, deliberately. move itself refuses before sending (move: cannot move into itself), which gives the better message and costs a round trip. fs refuses it too, because a check in the caller is a convention and only a check in the owner is an enforcement: fs owns the tree, and owns the bitmap rebuild that depends on the tree being acyclic. Leaving that to the client means the next client - a script, another service, a refactor of this one

  • inherits an obligation nobody told it about.

fs proves the predicate on every boot (fs: path guard selftest PASS), including the case a sloppy prefix test gets wrong: /ab is not inside /a, and moving it there must succeed.

4. Failure

fs now sends the REASON back with the failure rather than only logging it, so these are its words, not a guess made at the prompt:

what happenedwhat you see
the source does not existmove: failed - source not found
the destination already existsmove: failed - dest exists
the destination's parent does not existmove: failed - dest path not found
the destination's parent is a filemove: failed - dest not a directory
moving a directory into itself or its subtreemove: cannot move into itself (the shell's own check, before the round trip)
storage is not availablestorage unavailable - do NOT run 'drives flash' (data may be intact; awaiting storage recovery)
no answer came back (a lost reply, or fs died holding it)move: OUTCOME UNKNOWN - ... it MAY HAVE SUCCEEDED. Not re-sent - ...

Success prints moved <src> → <dst>.

This table used to say the first two were indistinguishable - "the same line; fs distinguishes them in its log". That was true and it was the wrong place to leave it: the reason was already in hand at the moment of failure and was being thrown away at the reply. See docs/persistence.md §6.17 and send_res! in services/fs. A client that has not been taught to read the reason still gets the old wording, because the reason is appended after an unchanged FS_ERR byte.

5. Later (separate doc so it can grow)

  • Cross-DRIVE move, which is genuinely copy-then-delete rather than a re-point, and therefore needs a progress report and an interruption story of its own.

6. Conformance

Conforms: own move help / move version (with a real example, per 0_conventions.md).