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
FTP CONNECT "files.example.com" AS "user" WITH "password" SET ?conn
FTP CONNECT "files.example.com" ON "2121" AS "user" WITH "password" SET ?conn
(* Default is 21 for plain FTP, 22 when SECURE selects SFTP *)
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
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
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.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.
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
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
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.
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:
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"
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"
/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.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 *)
FTP ?src TRANSFER ... TO ?dst below does it directly, and does not touch /external./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.
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".?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
FTP ?conn CLOSE
Full Verb Reference
| Verb | Description |
|---|---|
FTP CONNECT "host" [ON "port"] AS "user" WITH "password" [SECURE] SET ?conn | Connect by password — FTP, or FTPS with SECURE |
FTP CONNECT "host" AS "user" KEY "/root/key" SECURE SET ?conn | Connect by key — SFTP |
FTP ?conn LIST "path" SET ?items | Directory listing — name, is_dir, size, modified |
FTP ?conn STAT "path" SET ?info | Metadata for one path |
FTP ?conn UPLOAD "/root/src" TO "/remote/dst" | Namespace to server |
FTP ?conn DOWNLOAD "/remote/src" INTO "/root/dst" SET ?path | Server 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 ?anchor | Server to server, non-blocking |
FTP STATUS ?anchor SET ?progress | Progress of a server-to-server transfer |
EXTERNAL BIND FTP ?conn SET ?status | Mount the connection as /external — every path-taking verb reaches it |
FTP ?conn CLOSE | Close 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.
NEW FTP ACCESS TO "/root" SET ?access
AFTER EMIT ?access("host") & ":" & ?access("port")
AFTER EMIT ?access("username")
AFTER EMIT ?access("password")
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")
/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.