obsidian/wiki/concepts/gcs-resumable-upload-pattern.md
2026-04-30 21:23:56 +01:00

4.5 KiB
Raw Blame History

title aliases tags sources created updated
GCS Resumable Upload Pattern
gcs-resumable-upload
gcs-direct-upload
gcs-chunked-upload
gcs
google-cloud
upload
architecture
apache
large-files
daily/2026-04-30.md
2026-04-30 2026-04-30

GCS Resumable Upload Pattern

For large file uploads that bypass the load balancer: browser initiates upload via backend, backend creates a GCS Resumable Session URI, returns it to the browser, and the browser uploads chunks directly to GCS — completely bypassing Apache and the load balancer.

Key Points

  • Browser uploads directly to GCS, not through Apache/LB — bypasses the LimitRequestBody constraint and LB timeouts
  • Backend creates the GCS session URI via service account credentials and returns it to browser
  • Session URI valid for 7 days (time to resume, not maximum upload time)
  • Chunk size must be a multiple of 256 KB; 8 MB is a practical default
  • 308 Resume Incomplete = chunk accepted, send next chunk; 200/201 = upload complete
  • Network interruption recovery: PUT session_uri with Content-Range: bytes */{total_size} and 0-byte body → server returns current offset in Range header
  • LimitRequestBody 2147483648 (2 GB) needed in Apache only for the small /upload/init init request path

Details

Flow Overview

Browser → POST /api/v1/jobs/upload/init
              ↓
         Backend creates GCS Resumable Session URI
              ↓
         Returns { session_uri, upload_id } to browser
              ↓
Browser → PUT <session_uri> (chunk 1, bytes 08388607)
              ← 308 Resume Incomplete (continue)
Browser → PUT <session_uri> (chunk 2, bytes 838860816777215)
              ← 308 Resume Incomplete (continue)
...
Browser → PUT <session_uri> (final chunk)
              ← 200 OK or 201 Created (upload complete)
Browser → POST /api/v1/jobs/start { upload_id }

Backend: Create Session URI

from google.cloud import storage

def create_resumable_session(filename: str, content_type: str) -> str:
    client = storage.Client()
    bucket = client.bucket("my-bucket")
    blob = bucket.blob(f"uploads/{filename}")
    
    # Returns a resumable upload session URI
    session_uri = blob.create_resumable_upload_session(
        content_type=content_type,
        timeout=300,
    )
    return session_uri

Frontend: Chunk Upload

const CHUNK_SIZE = 8 * 1024 * 1024; // 8 MB — must be multiple of 256 KB

async function uploadChunked(sessionUri: string, file: File): Promise<void> {
  let offset = 0;
  
  while (offset < file.size) {
    const chunk = file.slice(offset, offset + CHUNK_SIZE);
    const isLast = offset + CHUNK_SIZE >= file.size;
    
    const response = await fetch(sessionUri, {
      method: "PUT",
      headers: {
        "Content-Range": `bytes ${offset}-${offset + chunk.size - 1}/${file.size}`,
        "Content-Type": file.type,
      },
      body: chunk,
    });
    
    if (response.status === 308) {
      // Chunk accepted — continue
      offset += chunk.size;
    } else if (response.status === 200 || response.status === 201) {
      // Upload complete
      break;
    } else {
      throw new Error(`Upload failed: ${response.status}`);
    }
  }
}

Resuming After Network Interruption

async function getUploadOffset(sessionUri: string, totalSize: number): Promise<number> {
  const response = await fetch(sessionUri, {
    method: "PUT",
    headers: {
      "Content-Range": `bytes */${totalSize}`,
      "Content-Length": "0",
    },
    body: null,
  });
  
  if (response.status === 308) {
    const range = response.headers.get("Range"); // "bytes=0-8388607"
    return parseInt(range!.split("-")[1]) + 1;
  }
  return 0; // No chunks received yet
}

Apache Configuration

# Only needed for the /upload/init endpoint (small JSON payload)
# The actual file chunks go directly to GCS, bypassing Apache
<Location /video-accessibility/api/v1/jobs/upload>
    LimitRequestBody 2147483648
</Location>

The 2147483648 value is 2 GB in bytes — for the small JSON init request this is academic, but Apache's default limit (usually 1 MB) would block even the metadata request on some configs.

Sources

  • daily/2026-04-30.md — Session 11:28, GCS resumable upload implementation for video-accessibility