File attachments¶
This page covers uploading, downloading, listing, and deleting the files in file attachment fields. Every
method here is also on the async client, where you await it.
Uploading¶
Set a file attachment field to a FileUpload in insert(), update(), or upsert().
from pathlib import Path
from pyquickbase import FileUpload, Quickbase
with Quickbase(realm="acme", token="your_user_token") as qb:
tasks = qb.app("bq1234567").table("Tasks")
upload = FileUpload("spec.pdf", Path("spec.pdf").read_bytes())
tasks.insert([{"Title": "Spec", "Attachment": upload}])
tasks.update([{"Record ID#": 7, "Attachment": upload}], timeout=120)
The other examples on this page use tasks, a table from a client made in the same way.
- A file field takes only a
FileUpload. Anything else raisesInvalidValueError, includingNone, so leave the field out to keep it empty. Quickbase would otherwise accept these mistakes without an error:Noneor""changes nothing, and a value without a file name creates a record with no file. - A
FileUploadsent to any other field raisesInvalidValueError, because Quickbase would store the file's name and discard its contents. - A file name can't contain a path or control characters. Quickbase stores names as sent, and passes them on to whoever downloads the file.
timeoutreplaces the client's timeout for that call, for a large file or a slow connection.
Versions¶
Each upload adds a version, and Quickbase silently removes the oldest once a field reaches its
limit. A new field keeps 3 versions, set by maxVersions in the field's properties. Uploading identical
contents still adds a version, so repeating an upload can push out a version you wanted.
| To keep | Pass to create_field() or update_field() |
|---|---|
| A different number | properties={"maxVersions": 10}, from 1 to 1,000 |
| Every version | properties={"versionMode": "keepallversions"}, which ignores maxVersions |
Lowering maxVersions removes nothing until the next upload, which then trims the field to the new
limit. Raising it doesn't bring back versions already removed.
Size limits¶
Quickbase refuses any request body over 40 MiB, with a 413 that raises APIError. Base64 adds a third,
so the largest file you can upload is about 31.4 MB. Writes are split into several requests to stay
under this limit, whatever the batch_size, and a file too large to fit is left for Quickbase to refuse.
If a later request fails, WriteError reports the rows already written.
Files are held in memory, as bytes and as base64, while they're sent.
Downloading¶
download_file() returns the latest version, or the one you ask for. It works on a read-only client.
file = tasks.download_file(7, "Attachment")
older = tasks.download_file(7, "Attachment", version=2, timeout=120)
print(file.file_name, len(file.data), file.content_type)
qb.request() can't read these responses, because they aren't JSON.
Don't use file.file_name as a path. Quickbase returns names exactly as they were uploaded, by anyone with
access and through any tool, so a name can be ../../.bashrc. Keep only the last part, and decide where
the file goes yourself:
from pathlib import Path, PureWindowsPath
name = PureWindowsPath(file.file_name).name # handles both / and \\
if name in ("", ".", ".."):
name = "download"
(Path("downloads") / name).write_bytes(file.data)
Likewise, file.content_type is based on the file's name, not its contents, and the contents are
whatever was uploaded. Treat both as untrusted, especially before opening or serving a file.
Listing versions¶
Reading a file field gives a FileAttachment, or None when the field has no file. Its versions are
FileVersion objects, oldest first, and latest is the newest.
task = tasks.get(7, select=["Attachment"])
if task["Attachment"] is not None:
for version in task["Attachment"].versions:
print(version.number, version.file_name, version.uploaded)
Each version names its uploader in creator, a User with their email address, so take care when
logging these values or passing them on. A User's repr leaves the email out, and a client's
policy leaves creator out unless a rule passes
user_emails=True.
Try it in the playground, with simulated data.
Deleting a version¶
tasks.delete_file_version(7, "Attachment", version=2)
version is required and must be at least 1, because Quickbase treats version 0 as the latest and would
delete it. Deleting the only version leaves the field empty and keeps the record. Version numbers aren't
reused or renumbered.
When something is refused¶
| What happened | Exception | What's left behind |
|---|---|---|
A file field set to anything but a FileUpload, or a FileUpload sent to another field |
InvalidValueError |
Nothing was sent |
| A file name with a path or control characters | InvalidValueError |
The FileUpload wasn't created |
A record ID or version that isn't a positive integer, a timeout that isn't a positive number, or a field that isn't a file field |
InvalidValueError |
Nothing was sent |
| A field label or ID that doesn't exist | UnknownFieldError |
Nothing was sent |
| A request over 40 MiB | APIError, status 413 |
Nothing in that request was written. Earlier batches were, and a WriteError says so |
| A record, version, or file that doesn't exist | NotFoundError |
Nothing was changed |
| A download from a field the role can't see | NotFoundError |
Nothing was changed |
| An upload the role can't do | WritePermissionError |
Nothing in the refused rows changed |
| Deleting a version the role can't delete | PermissionDeniedError |
Nothing was changed |