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 size bytes of mime-type with the given file metadata. Returns the session URI (from Location); 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