Skip to content

dctl moveto

Move a single file or directory to an exact destination name.

Synopsis#

dctl moveto is copyto's destination semantics with move's ordering guarantee. DEST names the object rather than the directory it lands in, and the source is removed only after the destination commit is durable.

sh
dctl moveto scratch/render.mov vault:films/2024/final.mov

This is the verb for promoting a finished artefact out of a working directory under its real name — the last step of a render pipeline, a build that ships, a database dump that becomes the day's backup. It is also the most dangerous command in the transfer family at the single-file scale: a wrong DEST moves the only copy of something somewhere you did not intend. It is classified destructive, it prompts under --interactive, and it refuses rather than guesses when the argument shapes are ambiguous.

The ordering is the product, and it is not re-implemented here. It lives in the same per-file pipeline move uses, so the two verbs cannot drift apart. PLAN.md §6 step 7: after the commit is durable, delete the source. The deletion sits on the far side of the transfer's result — the transfer must return Ok before the deletion is even reachable — so a crash, a network failure, a bad checksum or a Ctrl-C leaves the source file exactly where it was. At every instant the process could be killed, the file is either still at the source or durably committed at the destination.

"Durable commit" is a concrete thing in each direction. Into a vault it means the object was sealed, written with a verified write and its index record committed — dctl-core returns Ok only after all three. To a local path it means the destination file was written and fsynced to stable storage. Only then does the removal become reachable.

  • A checksum mismatch aborts before the commit, so the source survives. For a vault destination the comparison is inside the verified write. If the backend stored something other than what was sent, nothing is committed, nothing is deleted, and the run exits 20 (checksum_mismatch).
  • A file that transferred but whose source deletion failed is reported as an error. The data is safe, but you now have two copies rather than one. Removal failures always name the side — source render.mov — so a failed destination write and a failed source removal are never confused.

DEST is a name, not a container. dctl moveto render.mov vault:films would try to create an object literally called films; that is why an existing directory destination is refused. Compare:

sh
dctl move   render.mov vault:films vault:films/render.mov
dctl moveto render.mov vault:films/final.mov vault:films/final.mov

Those arrows describe the addressing that runs: a vault destination is reachable and the whole path after the colon is applied, so the second line stores films/final.mov. Both halves of that sentence used to say the opposite — see What runs today.

For a directory source the two coincide, exactly as in copyto: a tree moved under an exact name is a tree whose destination root is that name, and the relative paths inside are preserved.

Refused argument shapes, each a usage error (exit 1) raised before anything is transferred and long before anything could be deleted:

  • A DEST that names no object — a bare vault: or /.
  • A DEST that already exists as a directory. It cannot be both the object's name and the place the object goes.
  • Source and destination are the same file. Without this guard, step 7 would delete what step 6 had just committed. Structural equality catches the obvious case and canonicalisation catches render.mov versus ./render.mov and a symlinked duplicate.
  • The size filters excluded the single named file. Transferring nothing and reporting success is the hardest failure to notice, so it is refused.

A no-op moveto deletes nothing. If the destination already holds a matching object, the command says nothing to move: the destination already matches and stops. Deleting the source on that basis alone would be a deletion the plan never announced.

Destructive classification. Under --interactive, moveto asks before it acts and requires the exact word yes; with no terminal available that is a usage error rather than a hang. --force approves without asking. Note what the default is: running without either flag does not prompt — typing the command is taken as consent, because prompting by default would break every script. A declined confirmation exits 25 (cancelled), never a silent zero.

Comparison, filters and failure policy are the family's, identical to copyto: nothing at the destination means copy; --ignore-existing then --update can skip; otherwise size and modification time decide by default with a one-second tolerance, content hashes under the global --checksum, size alone under --size-only. --max-depth, --min-size and --max-size are honoured, as are --include, --exclude, --filter-from and --files-from, all through one engine. A skip is also a non-deletion: the source is only ever removed for a file whose own commit succeeded.

--dry-run is authoritative and deletes nothing. The plan is computed without touching either side, a dry run returns before the confirmation and before the reaper, and the same value is either printed or executed. A rename shows both paths:

sh
Action      Size  Path
------  --------  --------------------
copy    1.91 MiB  big.mov -> final.mov

What runs today#

Between two local paths, moveto works end to end. The file is written under its new name, flushed to stable storage, and only then removed from the source. A run that fails anywhere before that leaves the source where it was.

Promoting into a vault works end to end. dctl moveto ./render.mov archive:films/2024/final.mov seals and commits the object at the full path it was given, and only then removes the local file:

console
$ dctl moveto ./render.mov archive:films/2024/final.mov
 Transferred: 11 B / 11 B, 100%
    Verified: 11 B checksum-matched
       Files: 1 / 1
$ ls render.mov
ls: render.mov: No such file or directory
$ dctl ls archive:
      11 B films/2024/final.mov

An earlier revision of this page said the reaper opened a second vault session, hit the index's single-writer lock and stopped at connect time with exit 23 — and recommended promoting in two steps instead. That defect is fixed; the two-step workaround is no longer needed.

Moving out of a vault works. dctl moveto archive:site-b/report.txt ./out2.txt writes the plaintext locally and then removes the vault object, in that order — the ordering guarantee applies to a vault source exactly as it does to a local one. An earlier revision of this page said a REMOTE:PATH source "cannot be planned" and stopped at exit 7; that was true and stopped being true without the page changing.

Remote-to-remote is still refused, and this one is real: a direct vault-to-vault path needs the re-encrypting transfer dctl-core does not expose, and a plain-to-plain path needs an engine holding two backends at once. Neither is scheduled by PLAN.md §11.

The family's refusals apply unchanged, all exit 7 and all before anything is deleted: a plain write into a directory that holds a vault, and --checksum against a plain object store, which cannot supply a plaintext hash. A size ceiling used to be among them and no longer is: the engine streams, so there is no size at which a transfer stops fitting in memory. --no-traverse still skips the destination lookup, and therefore also skips the "DEST is an existing directory" refusal.

--immutable is honoured, at plan time: if DEST already names an object that would be replaced, the plan's single entry is an update and the run fails with exit 7 before any byte moves, naming the path. It governs the destination only and protects no source — deleting the source is what moveto means; use copyto to leave it in place. --immutable with --no-traverse is a usage error (exit 1), because --no-traverse skips the destination lookup and an unlisted destination cannot be checked for overwrites.

--transfers, --bwlimit and --retries are parsed and not consulted.

sh
dctl moveto SOURCE DEST [flags]

Examples#

The stderr run summary and the structured ERROR log line are omitted below except where they are the point.

Promote a finished render out of a scratch directory under its real name. The source is removed only after the destination write is flushed to stable storage:

console
$ dctl moveto ./scratch/render.mov /srv/films/2024/final.mov --force -v
1 to copy, 0 to update, 0 to delete, 0 unchanged (1.91 MiB)
 
 Transferred: 1.91 MiB / 1.91 MiB, 100%, 51 MiB/s
    Verified: 1.91 MiB checksum-matched
       Files: 1 / 1
      Checks: 1 / 1
      Errors: 0
     Elapsed: 0s
$ ls ./scratch/render.mov
ls: ./scratch/render.mov: No such file or directory
$ echo $?
0

The same promotion into a vault stops at connect time, with the render still in ./scratch — see What runs today for the two-step alternative:

console
$ dctl moveto ./scratch/render.mov archive:final.mov --no-traverse --force
error: index database error: Database already open. Cannot acquire lock.
$ ls ./scratch/render.mov
./scratch/render.mov
$ echo $?
23

Preview it first — always, for this verb. The plan names both paths, which is the thing to check:

console
$ dctl moveto /mnt/render/big.mov /srv/archive/final.mov --dry-run -v
1 to copy, 0 to update, 0 to delete, 0 unchanged (1.91 MiB)
Action      Size  Path
------  --------  --------------------
copy    1.91 MiB  big.mov -> final.mov

Move a Windows build artefact under a versioned name. D:\build\out.zip is a local path — a one-character prefix is a drive letter on every platform, so it is never read as a remote called D:

console
$ dctl moveto D:\build\out.zip E:\releases\apollo-2.1.0.zip --dry-run

A destination that is already a directory is refused, before anything is deleted:

console
$ dctl moveto render.mov /srv/films
error: '/srv/films' is a directory
warning: An exact-name transfer needs the destination's full object name. Use
'copy' to place the file inside a directory instead.
$ echo $?
1

Moving a file onto itself is refused. Without this guard, step 7 would delete what step 6 had just committed:

console
$ dctl moveto render.mov ./render.mov --force
error: source and destination are the same: render.mov
warning: A transfer onto itself would compare a tree against itself while
modifying it.
$ echo $?
1

Approving without a terminal. --interactive refuses rather than hanging, which is what an unattended job needs; --force is the scriptable yes:

console
$ dctl moveto ./scratch/dump.sql /srv/backups/2024-06-01.sql --interactive < /dev/null
error: cannot confirm 'move (deleting the source of)' on './scratch': no terminal available
warning: Pass --force to approve destructive actions non-interactively.
$ echo $?
1

Note that the prompt names ./scratch, the source's container, not ./scratch/dump.sql. Plan paths are relative to a root, and the root of a single-file transfer is the directory that holds it; the plan itself, which --dry-run prints, names the file.

An already-matching destination is a success, and the source stays put. The message is commentary, so it appears at -v and above:

console
$ dctl moveto ./dump.sql /srv/archive/dump.sql --size-only -v
0 to copy, 0 to update, 0 to delete, 1 unchanged (0 B)
nothing to move: the destination already matches
$ echo $?
0

Options#

sh
      --ignore-existing  Skip files that already exist at the destination, without comparing them
      --update           Skip files where the destination is newer than the source
      --no-traverse      Do not list the destination; assume every source file is missing there
  -h, --help             Print help (see more with '--help')
  -V, --version          Print version

Positional arguments:

ArgumentMeaning
<SOURCE>A local path, or REMOTE:PATH. Deleted only after a durable commit at the destination.
<DEST>Named exactly: the object's full path, not the directory it goes in.

There is deliberately no --create-empty-src-dirs, matching copyto.

--ignore-existing and --update are worth a second look here, because a skip under moveto is also a non-deletion: a file the plan skips is neither transferred nor removed from the source.

Options inherited from parent commands#

Every global flag is accepted; see ../GLOBAL_FLAGS.md for the full list. The ones that change what this command does:

FlagEffect here
-n, --dry-runPrint the plan and change nothing. Returns before the confirmation and before any deletion.
--forceApproves the destructive confirmation without prompting. Conflicts with --interactive.
-i, --interactivePrompts before the move and requires typing yes. With no terminal, exits 1 rather than hanging.
--checksumCompare content hashes instead of size and time. Refuses with exit 7 today when a hash is unavailable. Conflicts with --size-only.
--size-onlyCompare size alone, ignoring timestamps.
--verify <MODE>checksum (default) adds nothing to the verified write; sample and strict are identical today and re-read the uploaded object in full; against a filesystem destination all three do nothing. A failure at this stage is what keeps the source in place.
--verify-samples <N>Parsed and not consulted: partial sampling does not exist yet.
--immutableHonoured at plan time for the destination: an existing DEST makes the entry an update, which fails the run with exit 7 before anything moves. It does not protect the source — deleting it is what moveto means. Refused with --no-traverse (exit 1).
--format, --jsonRender as a table, one JSON document, or one JSON Lines record per action. Both JSON forms carry a result object on a real run — the executor's own counters, including errors — so what was attempted can be told from what was achieved.
--min-size, --max-sizeHonoured. If they exclude the single named file, that is a usage error rather than a silent no-op.
--include, --exclude, --filter-from, --files-fromHonoured. A file excluded by a rule is not moved and not deleted. A rule file that cannot be read or parsed is a usage error (exit 1) naming the file and the line, never a run with the rules dropped.
--transfers, --bwlimit, --retriesParsed and not consulted.
-P, --progressA per-file bar showing the real pipeline stage; a row at verify has been written but is not yet counted as stored, and its source is still in place.

Exit codes#

See ../EXIT_CODES.md for the full contract.

CodeNameWhen
0successThe object was transferred and its source removed, or a --dry-run completed, or the destination already held a matching object.
1usageUnparseable command line; DEST names no object (a bare root); DEST is an existing directory; source and destination are the same file; the size filters excluded the single named file; an unparseable or unsatisfiable size range; --interactive with no terminal; --immutable together with --no-traverse.
3dir_not_foundSOURCE does not exist.
5temporary_errorA cloud backend failed in a way worth retrying; the source is untouched. Reachable wherever a cloud backend is contacted: reading a plain b2:/s3:/r2: source, writing a plain object into one, or a vault whose store is one of them.
6partial_failureA directory source finished with at least one file failing. A file that failed keeps its source; a file whose source removal failed exists twice, and the message names the side.
7fatal_errorDEST is inside a local directory holding a vault; --checksum against a plain object store, which cannot supply a plaintext hash; both sides are remotes; --immutable and DEST already exists. Nothing was transferred and nothing was deleted.
20checksum_mismatchThe backend stored the wrong bytes. Nothing was committed and the source is untouched. Not reachable today: no moveto reaches a vault transfer, and a local write has no second party to disagree with.
21integrity_failure--verify sample/strict could not authenticate what was written. The source is untouched; investigate before removing it by hand. Not reachable today, for the same reason as 20.
22vault_lockedNo password was available, or the envelope did not unwrap. Nothing was transferred or deleted.
23index_errorThe index database could not be opened or committed — including every moveto into a vault today; see What runs today. Nothing was transferred or deleted.
25cancelledThe confirmation was declined, or the run was interrupted with Ctrl-C. Nothing was deleted.

Exit 20 carries the specific promise: the destination stored the wrong bytes, nothing was committed, and the source is untouched.

See also#

  • dctl copyto — the same destination semantics, without deleting the source.
  • dctl move — move into a container, keeping each object's own name.
  • dctl copy — the safe verb: add and update, never remove.
  • dctl deletefile — remove exactly one named object.
  • dctl verify — prove afterwards that what arrived is intact.