Uploads: binary-safe multipart uploads for small files, resumable
uploads (chunked, with status query and resume) for large ones, and
content replacement for existing files. upload-file and
update-file-content switch to resumable automatically above 5 MiB.
Content may be Bytes (sent verbatim), a Str (its UTF-8 bytes), or a Map/Vec (sent as JSON). Bodies are assembled as Bytes, so binary data (0x00, 0xFF, invalid UTF-8) arrives intact.
Resumable uploads. A session is opened with
start-resumable-upload, then content is PUT in chunks that are
multiples of 256 KiB (default 8 MiB, opts.chunk-size). After each
chunk, Drive's 308 Resume Incomplete says how much it has stored
(its Range header), and the next chunk starts from there. When a
chunk fails with 429/5xx or a dropped connection, the upload asks Drive
for the stored offset (upload-status) and carries on from it, once,
at once (google-core never waits; use a run retry policy for
backoff). Sessions stay valid for a week; resume-upload continues an
interrupted one, in this run or a later one.
A session URI is sent the call's credentials, so it must be a Google
URL (or under google.drive.api-root) unless opts has its own
credential: context credentials never go to another host.
Scopes. drive by default; drive.file is enough to create files
and to update files the app created.
Values
CHUNK_ALIGN
CHUNK_ALIGN: Int 262144
CONVERT_TARGETS
CONVERT_TARGETS: Map {
document: "application/vnd.google-apps.document",
doc: "application/vnd.google-apps.document",
spreadsheet: "application/vnd.google-apps.spreadsheet",
sheet: "application/vnd.google-apps.spreadsheet",
presentation: "application/vnd.google-apps.presentation",
slides: "application/vnd.google-apps.presentation"
}
DEFAULT_CHUNK_SIZE
DEFAULT_CHUNK_SIZE: Int 8388608
GOOGLE_SESSION_URI_PATTERN
GOOGLE_SESSION_URI_PATTERN: Str "^https://[A-Za-z0-9.-]+\\.googleapis\\.com/"
RESUMABLE_THRESHOLD
RESUMABLE_THRESHOLD: Int 5242880
UPLOAD_LOCAL_KEYS
UPLOAD_LOCAL_KEYS: Vec [
"parent",
"parents",
"description",
"metadata",
"resumable",
"chunk-size",
"convert-to",
"file-id"
]
Functions
align-chunk-size
fn (size: Int): Int
Round a chunk size down to a multiple of 256 KiB (Drive's requirement for every chunk but the last), at least 256 KiB.
build-multipart
fn (metadata: Map, content: Any, mime-type: Str, boundary: Str): Bytes
Build a multipart/related upload body as Bytes: a JSON metadata
part (nulls dropped) and the content part, separated by boundary.
Content is concatenated as bytes, never interpolated into a string,
so binary payloads survive.
chunk-bounds
fn (offset: Int, chunk-size: Int, total: Int): Vec
The [start, end) of the chunk beginning at offset (pure): end is offset + chunk-size, capped at total.
classify-upload-response
fn (response: Result): Map
Interpret the response to a resumable-upload PUT (pure): returns
{state: "complete", file}, {state: "incomplete", next-offset}
(from the Range header of a 308), or {state: "retry", error} for
failures worth another try (error is what to err if there are no
tries left: an HttpError or a transport message), or an Err (expired
session, other 4xx).
content-range
fn (start: Int, end: Int, total: Int): Str
The Content-Range for sending bytes start up to (not including)
end of a total-byte upload: content-range(0, 524288, 2000000) is
"bytes 0-524287/2000000". An empty range (start = end) is the
status query form "bytes */<total>".
convert-target
fn (value: Any): Str?
The Google-native MIME type to convert an upload into: "document"/"doc", "spreadsheet"/"sheet", "presentation"/"slides", or a full MIME type. Null means no conversion.
new-boundary
fn (): Str
A random multipart boundary, so file content cannot collide with it.
next-offset-from-range
fn (range: Str?): Int
The next byte to send after a 308 Resume Incomplete, from its Range header: "bytes=0-42" means 43. No header means nothing was stored (0).
put-range
fn (session-uri: Str, chunk: Bytes, start: Int, total: Int, opts: Map): ::hot::http/HttpResponse
resume-upload
fn (session-uri: Str, content: Any): Map
fn (session-uri: Str, content: Any, opts: Map): Map
Continue an interrupted resumable upload of content (the same bytes
as before): ask Drive for the stored offset and send the rest. Returns
the file resource, straight away if Drive already has every byte. An
expired session (sessions last a week) is an Err: start a new upload.
session ::google::drive::upload/start-resumable-upload({name: "big.bin"}, "application/octet-stream", length(bytes))
// ... store `session`; the upload is interrupted ...
file ::google::drive::upload/resume-upload(session, bytes)
send-content
fn (file-id: Str?, metadata: Map, bytes: Bytes, mime-type: Str, opts: Map): Map
session-put
fn (session-uri: Str, range: Str, body: Any, opts: Map): ::hot::http/HttpResponse
session-uri-error
fn (session-uri: Str, opts: Map): Str?
Why session-uri may not be used with these opts, or null: it is a Google URL or under google.drive.api-root, or opts has its own credential. Context credentials are never sent to any other URL.
start-resumable-upload
fn (metadata: Map, mime-type: Str, size: Int): Str
fn (metadata: Map, mime-type: Str, size: Int, opts: Map): Str
POST (or PATCH, with opts.file-id) /upload/drive/v3/files?uploadType=resumable
- open a resumable session for
sizebytes ofmime-typewith the given filemetadata. Returns the session URI (fromLocation); store it to resume later.
to-bytes
fn (content: Any): Bytes
Upload content as Bytes: Bytes pass through, a Str becomes its UTF-8 bytes, null is empty, and anything else is JSON-encoded.
update-file-content
fn (id: Str, content: Any, mime-type: Str): Map
fn (id: Str, content: Any, mime-type: Str, opts: Map): Map
PATCH /upload/files/{id} - replace a file's content (a new revision), optionally changing metadata via opts.metadata / description. Resumable above 5 MiB. Returns the file resource.
upload-file
fn (name: Str, content: Any, mime-type: Str): Map
fn (name: Str, content: Any, mime-type: Str, parent: Str): Map
fn (name: Str, content: Any, mime-type: Str, opts: Map): Map
Create a file with content. A Str fourth argument is the parent folder
id; an opts Map takes parent/parents, description, metadata
(extra fields such as appProperties), convert-to ("document",
"spreadsheet", "presentation": convert on upload, e.g. a .docx or
.csv into a Google Doc/Sheet), resumable: true to force a resumable
upload, chunk-size, Drive parameters (keepRevisionForever,
ocrLanguage, useContentAsIndexableText, fields) and
credentials. Returns the file resource.
file ::google::drive::upload/upload-file("report.csv", csv-text, "text/csv", folder-id)
sheet ::google::drive::upload/upload-file("report", csv-text, "text/csv", {parent: folder-id, convert-to: "spreadsheet"})
upload-loop
fn (session-uri: Str, bytes: Bytes, offset: Int, chunk-size: Int, attempt: Int, policy: Map, opts: Map): Map
upload-resumable
fn (session-uri: Str, content: Any): Map
fn (session-uri: Str, content: Any, opts: Map): Map
Send content to an open session URI, starting at byte 0 (or
opts.offset), in opts.chunk-size chunks (rounded down to 256 KiB
multiples; default 8 MiB), following the offset Drive reports after
each one. Returns the created/updated file resource.
upload-status
fn (session-uri: Str, total: Int): Map
fn (session-uri: Str, total: Int, opts: Map): Map
Ask Drive how much of a total-byte resumable upload it has stored
(an empty PUT with Content-Range: bytes */total). Returns
{complete, next-offset, file}: next-offset is the next byte to
send (from the 308's Range header; 0 when nothing is stored), and
file is the file resource once complete. An expired session or a
failed query is an Err (::google::api/is-retryable says whether to
try again later).
status ::google::drive::upload/upload-status(session-uri, length(bytes))
status.next-offset