<%@ Page Title="Strategy: Direct-to-GCS resumable" Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true" %>
<%@ Import Namespace="AjaxUploader.Providers" %>
<asp:Content ID="TitleContent" ContentPlaceHolderID="TitleContent" runat="server">Direct-to-GCS resumable - AjaxUploader v5.2 Demos</asp:Content>
<asp:Content ID="HeaderContent" ContentPlaceHolderID="HeaderTitle" runat="server">Direct-to-GCS resumable</asp:Content>
<asp:Content ID="BodyContent" ContentPlaceHolderID="MainContent" runat="server"> 
<div class="demo-card"> <div class="demo-card-header">Browser-direct Google Cloud Storage resumable upload</div> <div class="demo-card-body"> <p class="demo-description"> The third member of the cloud-direct trio (alongside S3 and Azure). The browser uploads each chunk straight to a GCS resumable session URL via <code>Content-Range</code> headers. Bytes never traverse IIS - the WebForms handler only signs the initiate. Survives reloads via IndexedDB: the resumed task issues a zero-byte PUT to the session URL, reads <code>Range: bytes=0-N</code> from GCS's 308 response, and continues from byte N+1.
 </p> <div class="d-flex gap-2 mb-2 mt-4">
 <button type="button" class="btn btn-primary" id="startBtn_gcsUploader">Start upload</button>
 <button type="button" class="btn btn-outline-secondary" id="clearBtn_gcsUploader">Clear queue</button>
 </div>
 <div id="gcsUploader"></div> <script> (function () {
 function init() {
 if (!window.AjaxUploader || typeof AjaxUploader.create !== 'function') return; // graceful no-op if runtime missing
 var startButton = document.getElementById('startBtn_gcsUploader');
 var clearButton = document.getElementById('clearBtn_gcsUploader');
 var uploader = AjaxUploader.create('#gcsUploader', {
 uploadUrl: '<%= ResolveUrl("~/ajaxupload.axd/upload") %>',
 strategy: 'gcs',
 chunkSize: 8 * 1024 * 1024, // GCS-aligned default; auto-rounded to 256 KiB grain
 chunkConcurrency: 1, // GCS resumable is protocol-sequential
 autoUpload: false,
 multiple: true,
 persistState: true,
 persistAdapter: 'indexeddb'
 });
 if (startButton) startButton.addEventListener('click', function () { if (uploader && typeof uploader.upload === 'function') uploader.upload(); });
 if (clearButton) clearButton.addEventListener('click', function () { if (uploader && typeof uploader.clear === 'function') uploader.clear(); });
 }
 if (document.readyState !== 'loading') init();
 else document.addEventListener('DOMContentLoaded', init);
 })();
 </script> <div class="alert alert-info mt-3" role="alert"> <strong>Resume behaviour:</strong> reload during an upload -> the session URL is restored from IndexedDB -> the client probes GCS with <code>Content-Range: bytes */&lt;total&gt;</code> -> GCS returns <code>308 Resume Incomplete</code> with the byte offset it has -> uploading continues. No bytes re-sent.
 </div> <h5 class="mt-4">Client config</h5> <div class="code-panel">
<pre><code>AjaxUploader.create('#uploader', {
 uploadUrl: '<%= ResolveUrl("~/ajaxupload.axd/upload") %>',
 strategy: 'gcs',
 chunkSize: 8 * 1024 * 1024, <span style="color:#94a3b8">// default 8 MiB; rounded to 256 KiB grain</span> persistState: true,
 persistAdapter: 'indexeddb'
});</code></pre> </div> <h5 class="mt-4">Wire protocol</h5> <ul class="mb-3"> <li><code>POST /ajaxupload.axd/gcs/initiate</code> -> server creates a GCS resumable session, returns <code>{ sessionUrl, key }</code></li> <li><code>PUT &lt;sessionUrl&gt;</code> <- chunked bytes with <code>Content-Range: bytes &lt;start&gt;-&lt;end&gt;/&lt;total&gt;</code> (browser-direct)</li> <li><code>308 Resume Incomplete</code> on every chunk except the last (with <code>Range: bytes=0-N</code> showing committed offset)</li> <li><code>200/201 OK</code> on the final chunk - body is the GCS object metadata</li> <li><code>POST /ajaxupload.axd/gcs/finalize</code> -> optional server-side bookkeeping</li> <li><code>POST /ajaxupload.axd/gcs/abort</code> on cancel / fatal error</li> </ul> <h5 class="mt-4">Server-side (Global.asax.cs)</h5> <p>Implement <code>IGcsSigner</code> against the official <code>Google.Cloud.Storage.V1</code> SDK or sign manually with a service account. Same shape as <code>IS3Signer</code> / <code>IAzureSigner</code>:</p> <div class="code-panel">
<pre><code>using AjaxUploader.Providers;

protected void Application_Start(object sender, EventArgs e)
{
 Gcs.Signer = new MyGcsSigner(
 bucketName: "your-bucket",
 serviceAccountKeyJson: ConfigurationManager.AppSettings["GCS:ServiceAccountKey"]);
}</code></pre> </div> <h5 class="mt-4">Bucket CORS</h5> <p>The browser PUTs directly to GCS, so the bucket must allow cross-origin PUT and expose the <code>Range</code> response header (used to read resume offsets):</p> <div class="code-panel">
<pre><code>[{
 "origin": ["https://ajaxuploader.com"],
 "method": ["PUT", "OPTIONS"],
 "responseHeader": ["Range", "Content-Range", "ETag"],
 "maxAgeSeconds": 3600
}]</code></pre> </div> <p class="text-muted small mt-2">Apply with <code>gsutil cors set cors.json gs://your-bucket</code>.</p> <h5 class="mt-4">When to pick GCS over S3</h5> <ul class="mb-3"> <li><strong>Cheaper egress to Google services</strong> (BigQuery, Vertex AI, Cloud Run) - same-region transfer is free.</li> <li><strong>Simpler protocol</strong> - one resumable session per file vs. S3's multipart sequence (initiate / sign N parts / complete).</li> <li><strong>Object versioning + soft delete</strong> are first-class on GCS buckets.</li> <li><strong>S3-compatible APIs need not apply</strong> - for MinIO / R2 / B2, use <code>strategy: 's3'</code> instead.</li> </ul> </div>
</div> 
</asp:Content>
