Receiving Uploads
A file arrives as bytes. There is no encoding step and nothing to decode - what was sent is what lands on disk. Two doors lead in, and both work from anywhere: a script does not have to be running in Site Mode to receive a file.
The first door — multipart and !FILES
Send the request as multipart/form-data instead of a form-encoded body. The ordinary fields are unchanged: identity, password and q travel as they always did. Any part carrying a filename becomes a file, and !FILES holds its bytes.
FILE WRITE !FILES('upload') TO "/root/uploads/" & !FILES('upload:name')
AFTER EMIT "kept " & !FILES('upload:size') & " bytes"
Alongside the bytes, two more keys describe what arrived. !FILES('x:name') is the filename the sender gave it and !FILES('x:size') is its length - so a script can name the file what it was called, or refuse it before writing.
curl -X POST https://ql.ocalt.com/api \
-F "identity=your_identity" \
-F "password=your_api_key" \
-F 'q=FILE WRITE !FILES("photo") TO "/root/photo.jpg" AFTER EMIT "saved"' \
-F "photo=@holiday.jpg"
const fd = new FormData();
fd.append('identity', 'your_identity');
fd.append('password', 'your_api_key');
fd.append('q', `FILE WRITE !FILES('f') TO "/root/" & !FILES('f:name')
AFTER EMIT "ok"`);
fd.append('f', fileInput.files[0]);
// No Content-Type header — the browser sets the boundary itself.
const out = await fetch('https://ql.ocalt.com/api', { method:'POST', body: fd })
.then(r => r.text());
import requests
requests.post('https://ql.ocalt.com/api',
data={'identity': 'your_identity', 'password': 'your_api_key',
'q': 'FILE WRITE !FILES("doc") TO "/root/report.pdf" AFTER EMIT "saved"'},
files={'doc': open('report.pdf', 'rb')})
Several at once
Each part is its own key, so a form with three file inputs arrives as three entries and the script decides what to do with each.
FILE WRITE !FILES('avatar') TO "/root/profile/avatar.png"
AFTER FILE WRITE !FILES('banner') TO "/root/profile/banner.png"
AFTER IF !FILES('resume') IS SET
OPEN
FILE WRITE !FILES('resume') TO "/mounted/resumes/" & !FILES('resume:name')
CLOSE
AFTER EMIT "profile updated"
Refusing one before it lands
The size is known before anything is written, so a file that is too large or the wrong kind never reaches storage.
NUMBER !FILES('upload:size') SET ?size
AFTER STRING !FILES('upload:name') SET ?name
AFTER IF ?size IS GREATER THAN 10485760
OPEN
STATUS 413
AFTER EMIT "That file is over 10MB."
CLOSE
OR
OPEN
IF ?name CONTAINS ".exe"
OPEN
STATUS 415
AFTER EMIT "Not that kind of file."
CLOSE
OR
OPEN
FILE WRITE !FILES('upload') TO "/root/uploads/" & ?name
AFTER EMIT "kept " & ?name
CLOSE
CLOSE
Outside Site Mode
!POST and !GET carry a real visitor's request only inside Site Mode, or when forwarded with the post and get fields. !FILES is different: it is filled by the shape of the request itself, so a file uploaded straight to the API is there whether or not anything else is.
That is what lets a backend of your own accept a file from its users and hand it on without ever writing it to its own disk - it forwards the part it received, and the script writes it into the namespace.
$ch = curl_init('https://ql.ocalt.com/api');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => [
'identity' => 'your_identity',
'password' => 'your_api_key',
'q' => 'FILE WRITE !FILES("f") TO "/root/inbox/" & !FILES("f:name")
AFTER EMIT "stored"',
// The file as it arrived, never touching this server's disk.
'f' => new CURLFile($_FILES['upload']['tmp_name'], null,
$_FILES['upload']['name']),
],
]);
echo curl_exec($ch);
The second door — /stream
Where the file is large or already moving, /stream takes the bytes as the whole request body. There is no script and no form - the path is in the query string and the body is the file.
curl -X POST "https://core.ocalt.com/stream?identity=your_identity&password=your_api_key&path=/root/video.mp4" \
--data-binary @video.mp4
curl "https://core.ocalt.com/stream?identity=your_identity&password=your_api_key&path=/root/video.mp4" \
-o video.mp4
Nothing is buffered into a script and nothing is encoded, so the size of the file is bounded by your storage rather than by anything in the request. This is the door the OcaltQL clients use to move files between a machine and a namespace.
Which door
!FILES | /stream | |
|---|---|---|
| Shape | multipart, alongside a script | raw body, no script |
| Runs a script | Yes - decide, validate, rename, resize | No - the bytes go where the path says |
| Several files | Yes, one part each | One per request |
| Suits | forms, avatars, documents, anything to be checked | large files, backups, video, machine to namespace |
| Cost | five queries | five queries |
What's next
File Manager for what to do with the file once it has landed, or Header & Globals for the rest of the request.