FTP & External Storage

FTP CONNECT authenticates to a file server and captures the connection with SET. Every subsequent operation is subject-first — FTP ?conn VERB ... — exactly like EMAIL and SSH. One verb covers plain FTP, FTPS and SFTP; SECURE is what selects an encrypted transport.

Connecting

Password Authentication
FTP CONNECT "files.example.com" AS "user" WITH "password" SET ?conn
Custom Port
FTP CONNECT "files.example.com" ON "2121" AS "user" WITH "password" SET ?conn
(* Default is 21 for plain FTP, 22 when SECURE selects SFTP *)
Encrypted Transport
FTP CONNECT "files.example.com" AS "user" WITH "password" SECURE SET ?conn
AFTER FTP CONNECT "files.example.com" AS "deploy" KEY "/root/keys/id_rsa" SECURE SET ?conn2
SECURE is the only switch. Without it the connection is plain FTP. With it, the transport is encrypted — FTPS when authenticating by password, SFTP when authenticating by KEY. There is no separate FTPS or SFTP verb to learn, and no protocol string to get wrong.

Listing and Inspecting

List a Directory
FTP CONNECT "files.example.com" AS "user" WITH "password" SET ?conn
AFTER FTP ?conn LIST "/public_html" SET ?items
AFTER FOREACH ?items SET ?item
OPEN
  EMIT ?item("name") & " — " & ?item("size") & " bytes"
CLOSE
Each entry carries name, is_dir, size and modified — the same shape FILE LIST returns for your own namespace and DIRECTIVE "id" LIST FROM returns for a machine, so all three can be walked by identical code.
Stat a Single Path
FTP ?conn STAT "/public_html/index.html" SET ?info
AFTER EMIT ?info("size")

Transferring Files

UPLOAD sends a file from your namespace to the server; DOWNLOAD brings one back. The prepositions are the same ones SSH uses, so the two verbs read identically.

Upload and Download
FTP ?conn UPLOAD "/root/build.zip" TO "/public_html/build.zip"
AFTER FTP ?conn DOWNLOAD "/public_html/logs/access.log" INTO "/mounted/logs/access.log" SET ?path
AFTER EMIT ?path
A Large Transfer, Without Blocking
FTP ?conn DOWNLOAD "/backups/nightly.tar" INTO "/mounted/nightly.tar" SET PROMISE ?job
AFTER EMIT "transfer started, script keeps running"
AFTER WAIT FOR ?job SET ?path
AFTER EMIT ?path

Organising

Create, Move, Delete
FTP ?conn MKDIR "/public_html/assets"
AFTER FTP ?conn MOVE "/public_html/old.html" TO "/public_html/archive/old.html"
AFTER FTP ?conn DELETE "/public_html/tmp.txt"

External Storage — /external

Everything above treats a server as somewhere to send files to and fetch files from. EXTERNAL BIND does something different: it mounts the connection as a real location in your namespace. After binding, /external sits alongside /root and /mounted and every OcaltQL verb that takes a path reaches it — not a subset, all of them.

Bind a Connection, Then Use Ordinary File Verbs
FTP CONNECT "files.example.com" AS "user" WITH "password" SET ?conn
AFTER EXTERNAL BIND FTP ?conn SET ?status
AFTER FILE READ "/external/file.txt" SET ?txtcontent
AFTER EMIT ?txtcontent

Nothing on that third line is FTP-aware. FILE READ does not know or care that the bytes come off a remote server — the path is what routes it. The same is true of every other verb:

The Whole Verb Surface, Against a Remote Server
FTP CONNECT "files.example.com" AS "user" WITH "password" SET ?conn
AFTER EXTERNAL BIND FTP ?conn SET ?status

AFTER FILE LIST "/external/reports" SET ?items
AFTER FILE WRITE "generated" TO "/external/reports/out.txt"
AFTER FILE COPY "/root/local.pdf" TO "/external/archive/local.pdf"
AFTER FILE MOVE "/external/old.csv" TO "/mounted/old.csv"
AFTER FILE STAT "/external/reports/out.txt" SET ?info
AFTER FILE DELETE "/external/tmp.txt"
Beyond FILE — Any Verb That Takes a Path
EDITOR OPEN "/external/app.js" SET ?doc
AFTER EDITOR ?doc REPLACE ALL "var " WITH "let " SET ?doc
AFTER EDITOR ?doc SAVE SET ?ok

AFTER IMAGE LOAD "/external/photos/raw.jpg" SET ?img
AFTER GRAPHIC ?img RESIZE WIDTH 800 SET ?img

AFTER MEDIA LOAD "/external/media/clip.mp4" SET ?media
AFTER MEDIA ?media TRIM FROM "00:00:10" TO "00:00:40" SET ?clip

AFTER FILE COMPRESS ZIP [FILE SELECT ALL "/external/reports"] TO "/root/reports.zip"
The bind is per execution, and the last one holds. /external names exactly one connection at a time. Bind twice in the same script and the second replaces the first — every path used after that point resolves against the new server, and paths used before it resolved against the old one. Nothing is stored: a later execution starts with nothing bound and must bind again. Inside a PERSIST timeline the bind holds for the life of the timeline, like any other execution state.
Two Servers in One Script
FTP CONNECT "hostA.example.com" AS "u1" WITH "p1" SET ?a
AFTER FTP CONNECT "hostB.example.com" AS "u2" WITH "p2" SET ?b

AFTER EXTERNAL BIND FTP ?a SET ?status
AFTER FILE COPY "/external/report.csv" TO "/root/from-a.csv"

AFTER EXTERNAL BIND FTP ?b SET ?status
AFTER FILE COPY "/root/from-a.csv" TO "/external/incoming/report.csv"
(* The second bind replaces the first — /external is hostB from that line on *)
Moving a file between two servers does not require binding at all — FTP ?src TRANSFER ... TO ?dst below does it directly, and does not touch /external.
Latency is real and the path hides it. A read from /external crosses a network; a read from /root does not. The verbs are identical, the cost is not — a loop that reads a hundred files from /external makes a hundred round trips. Pull what you need to /root once with FILE COPY when you are going to work on it repeatedly.

Server to Server

Two open connections can pass a file directly between them, without the bytes travelling through your namespace at all. TRANSFER is non-blocking — it returns a status anchor immediately, read with FTP STATUS, the same shape DIRECTIVE NETWORK TRANSFER uses between machines.

Move a File Between Two Servers
FTP CONNECT "hostA.example.com" AS "u1" WITH "p1" SET ?src
AFTER FTP CONNECT "hostB.example.com" AS "u2" WITH "p2" SECURE SET ?dst
AFTER FTP ?src TRANSFER "/exports/data.zip" TO ?dst "/imports/data.zip" SET ?anchor
AFTER FTP STATUS ?anchor SET ?progress
AFTER IF ?progress IS IDENTICAL TO "done"
OPEN
  EMIT "server-to-server transfer complete"
CLOSE
OR
OPEN
  EMIT "still transferring — check back later"
CLOSE
FTP STATUS takes the anchor, not a connection. A server-to-server transfer belongs to neither endpoint, so its progress is read from the anchor itself — the same reason DIRECTIVE STATUS ?anchor is distinct from DIRECTIVE STATUS "id".
A transfer holds both of its connections. The bytes move between the two servers directly, so both ?src and ?dst are in use for as long as it takes. A later statement that touches either one waits for the transfer to finish — which is not what non-blocking means to a script that expected to keep working. Open a separate connection for anything you want to do meanwhile; the two are independent, and nothing stops you holding several at once.

Closing

Close a Connection
FTP ?conn CLOSE

Full Verb Reference

Verb Description
FTP CONNECT "host" [ON "port"] AS "user" WITH "password" [SECURE] SET ?connConnect by password — FTP, or FTPS with SECURE
FTP CONNECT "host" AS "user" KEY "/root/key" SECURE SET ?connConnect by key — SFTP
FTP ?conn LIST "path" SET ?itemsDirectory listing — name, is_dir, size, modified
FTP ?conn STAT "path" SET ?infoMetadata for one path
FTP ?conn UPLOAD "/root/src" TO "/remote/dst"Namespace to server
FTP ?conn DOWNLOAD "/remote/src" INTO "/root/dst" SET ?pathServer to namespace — accepts SET PROMISE
FTP ?conn MKDIR "path"Create a directory
FTP ?conn MOVE "from" TO "to"Move or rename
FTP ?conn DELETE "path"Delete a file
FTP ?src TRANSFER "path" TO ?dst "path" SET ?anchorServer to server, non-blocking
FTP STATUS ?anchor SET ?progressProgress of a server-to-server transfer
EXTERNAL BIND FTP ?conn SET ?statusMount the connection as /external — every path-taking verb reaches it
FTP ?conn CLOSEClose the connection

Your Namespace as an FTP Server

Everything above connects OcaltQL out to a file server. This is the other direction: your own namespace, reachable over FTP, from FileZilla or any other client.

Minting Access
NEW FTP ACCESS TO "/root" SET ?access
AFTER EMIT ?access("host") & ":" & ?access("port")
AFTER EMIT ?access("username")
AFTER EMIT ?access("password")
Scoping It to a Folder
NEW FTP ACCESS TO "/mounted/media" SET ?access
(* The client sees that folder as its root and cannot climb above it *)

AFTER FTP ACCESS LIST SET ?all
AFTER FTP ACCESS REVOKE ?access("id")
Access is granted to /root or /mounted, or to any folder inside them. The client is confined to whatever was granted — it is the same namespace boundary every OcaltQL file verb obeys, enforced at the FTP layer. /mounted requires Starter or above, since Free has no mounted storage.
Credentials are minted per grant and revocable. Revoking drops any connected session immediately. They are not your Ocalt account password and cannot be used to sign in anywhere else.