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.

An upload costs five queries. Whichever door it comes through, receiving a file is charged as five, not one. It is the one operation where the work is in the bytes rather than in the statement.

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.

Receiving one file
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
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"
JavaScript, from a browser
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());
Python
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.

Three parts, three decisions
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.

A size and a type check
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.

PHP, passing a visitor's upload through
$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.

Straight in
curl -X POST "https://core.ocalt.com/stream?identity=your_identity&password=your_api_key&path=/root/video.mp4" \
  --data-binary @video.mp4
And back out
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
Shapemultipart, alongside a scriptraw body, no script
Runs a scriptYes - decide, validate, rename, resizeNo - the bytes go where the path says
Several filesYes, one part eachOne per request
Suitsforms, avatars, documents, anything to be checkedlarge files, backups, video, machine to namespace
Costfive queriesfive queries
Bytes are bytes in both. BASE64 exists for the case where a field cannot carry binary at all - not for uploads, which have their own door.

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.