<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Shawn K. Lewis</title>
    <description>The latest articles on DEV Community by Shawn K. Lewis (@shawnklewis).</description>
    <link>https://dev.to/shawnklewis</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3392536%2F033212c4-cd99-41bc-b0ec-efc1edddb9cb.jpg</url>
      <title>DEV Community: Shawn K. Lewis</title>
      <link>https://dev.to/shawnklewis</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shawnklewis"/>
    <language>en</language>
    <item>
      <title>gdup — a C# CLI for uploading huge files to Google Drive (and surviving interruptions)</title>
      <dc:creator>Shawn K. Lewis</dc:creator>
      <pubDate>Sat, 17 Oct 2026 22:09:35 +0000</pubDate>
      <link>https://dev.to/shawnklewis/gdup-a-c-cli-for-uploading-huge-files-to-google-drive-and-surviving-interruptions-12ao</link>
      <guid>https://dev.to/shawnklewis/gdup-a-c-cli-for-uploading-huge-files-to-google-drive-and-surviving-interruptions-12ao</guid>
      <description>&lt;p&gt;Uploading a 50 GB file to Google Drive through the browser works fine, right up until the Wi‑Fi blips at 87%. Then you start from zero.&lt;/p&gt;

&lt;p&gt;So I wrote &lt;strong&gt;gdup&lt;/strong&gt;: a small Windows command-line tool in C# that uploads one large file to Google Drive as fast as a single stream allows, and picks up where it left off when something breaks.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/shawnklewis/GoogleDriveResumableUploader" rel="noopener noreferrer"&gt;https://github.com/shawnklewis/GoogleDriveResumableUploader&lt;/a&gt; (.NET 10, Windows)&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resumable uploads.&lt;/strong&gt; Ctrl+C, a crash, or a dropped connection doesn't lose progress. Re-run the same command (or &lt;code&gt;gdup resume &amp;lt;file&amp;gt;&lt;/code&gt;) and it continues.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network interface pinning.&lt;/strong&gt; &lt;code&gt;--via "Ethernet 2"&lt;/code&gt; forces the upload onto a specific NIC. Handy when one machine has a fast wired link and a slow default route.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checksum verification&lt;/strong&gt;, with its own exit code when the hashes don't match.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encrypted credentials.&lt;/strong&gt; Config is stored DPAPI-encrypted under &lt;code&gt;%APPDATA%\gdup\gdup.conf&lt;/code&gt;, in an rclone-style format. You can import an existing Drive remote straight from your &lt;code&gt;rclone.conf&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verifiable releases.&lt;/strong&gt; GitHub Actions builds, tests and publishes &lt;code&gt;gdup.exe&lt;/code&gt; with a SHA-256 checksum and a signed build-provenance attestation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gdup config                       &lt;span class="c"&gt;# create a remote (bring your own Desktop-app OAuth client)&lt;/span&gt;
gdup config import-rclone         &lt;span class="c"&gt;# or reuse a Drive remote from rclone.conf&lt;/span&gt;
gdup nics                         &lt;span class="c"&gt;# list interfaces for --via&lt;/span&gt;

gdup upload D:&lt;span class="se"&gt;\v&lt;/span&gt;ideo&lt;span class="se"&gt;\b&lt;/span&gt;ig.mkv gdrive:Backups/2026
gdup upload D:&lt;span class="se"&gt;\v&lt;/span&gt;ideo&lt;span class="se"&gt;\b&lt;/span&gt;ig.mkv gdrive:Backups &lt;span class="nt"&gt;--via&lt;/span&gt; &lt;span class="s2"&gt;"Ethernet 2"&lt;/span&gt; &lt;span class="nt"&gt;--chunk-size&lt;/span&gt; 512M &lt;span class="nt"&gt;--stall-timeout&lt;/span&gt; 90

gdup resume &lt;span class="nt"&gt;--list&lt;/span&gt;
gdup resume D:&lt;span class="se"&gt;\v&lt;/span&gt;ideo&lt;span class="se"&gt;\b&lt;/span&gt;ig.mkv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exit codes are scriptable: &lt;code&gt;0&lt;/code&gt; success, &lt;code&gt;1&lt;/code&gt; usage/config error, &lt;code&gt;2&lt;/code&gt; upload failed, &lt;code&gt;3&lt;/code&gt; checksum mismatch.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Drive's resumable upload protocol works
&lt;/h2&gt;

&lt;p&gt;This is the part that makes the whole thing possible, and it's simpler than it sounds:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start a session.&lt;/strong&gt; POST the file's metadata to the upload endpoint with &lt;code&gt;uploadType=resumable&lt;/code&gt;. Drive replies with a &lt;strong&gt;session URI&lt;/strong&gt; in the &lt;code&gt;Location&lt;/code&gt; header. That URI &lt;em&gt;is&lt;/em&gt; the upload. Keep it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send chunks.&lt;/strong&gt; PUT byte ranges to the session URI with a &lt;code&gt;Content-Range: bytes start-end/total&lt;/code&gt; header. Every chunk except the last must be a multiple of 256 KiB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drive acknowledges progress.&lt;/strong&gt; For each intermediate chunk it answers &lt;code&gt;308 Resume Incomplete&lt;/code&gt; with a &lt;code&gt;Range&lt;/code&gt; header telling you what it has actually stored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recover after a failure.&lt;/strong&gt; Send an empty PUT with &lt;code&gt;Content-Range: bytes */total&lt;/code&gt;. Drive tells you how many bytes it has, and you continue from there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Finish.&lt;/strong&gt; The final chunk returns &lt;code&gt;200&lt;/code&gt;/&lt;code&gt;201&lt;/code&gt; with the file's metadata.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key insight: the server, not the client, is the source of truth for progress. As long as you keep the session URI, you can come back later and ask "where were we?"&lt;/p&gt;

&lt;h2&gt;
  
  
  How gdup builds on that
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Sessions survive the process.&lt;/strong&gt; As soon as Drive hands back a session URI, gdup writes it to a small JSON file under &lt;code&gt;%LOCALAPPDATA%\gdup\sessions&lt;/code&gt;. The file name is a SHA-256 of the file's path, size and last-write time plus the destination remote, folder and name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;KeyFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;fullPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;mtimeTicks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                            &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;folderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"|"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fullPath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToUpperInvariant&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mtimeTicks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;folderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToHexStringLower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SHA256&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;HashData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the file changes on disk, the key changes, so gdup never resumes a stale session against different bytes. Leftover state for older versions of the same file is cleaned up automatically. Writes go to a &lt;code&gt;.tmp&lt;/code&gt; file first and are then moved into place, so a crash mid-save can't leave a half-written state file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One loop, three outcomes.&lt;/strong&gt; Every response is interpreted the same way: &lt;code&gt;200&lt;/code&gt;/&lt;code&gt;201&lt;/code&gt; means done, &lt;code&gt;308&lt;/code&gt; means "continue from this offset", and &lt;code&gt;404&lt;/code&gt;/&lt;code&gt;410&lt;/code&gt; means Drive has thrown the session away. An expired session is dropped and a fresh one is started from byte zero. If that happens three times in a row with no progress, gdup gives up instead of looping forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retries back off with jitter.&lt;/strong&gt; Transient failures (network errors, timeouts, retryable Drive errors) retry with exponential backoff capped at 64 seconds, half fixed and half random. Local disk read errors get only a few attempts: a USB hiccup can clear on retry, but a bad block won't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A watchdog for stalls.&lt;/strong&gt; A connection can hang without ever erroring. A watchdog tracks bytes leaving the socket; if nothing moves for &lt;code&gt;--stall-timeout&lt;/code&gt; seconds (default 60), it cancels the chunk and resumes from Drive's committed offset. Once a chunk's body is fully sent, it allows longer (at least five minutes) for Drive's reply, since Drive can take a while to acknowledge a large chunk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chunk sizes are validated up front.&lt;/strong&gt; &lt;code&gt;--chunk-size&lt;/code&gt; defaults to 256 MiB and must be a multiple of 256 KiB, up to 4 GiB, so a bad value fails at the command line instead of halfway through an upload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;--via&lt;/code&gt; binds the socket.&lt;/strong&gt; gdup uses a single &lt;code&gt;HttpClient&lt;/code&gt; whose &lt;code&gt;SocketsHttpHandler.ConnectCallback&lt;/code&gt; binds each socket to the chosen interface's local IP before connecting, and Windows then routes the traffic out of that NIC. Auto-redirect is off, because Drive's &lt;code&gt;308 Resume Incomplete&lt;/code&gt; is not a redirect and must not be followed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The checksum runs in parallel.&lt;/strong&gt; The local MD5 is computed alongside the upload, then compared with the &lt;code&gt;md5Checksum&lt;/code&gt; Drive reports for the finished file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that surprised me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You can't combine network links for one file.&lt;/strong&gt; I hoped &lt;code&gt;--via&lt;/code&gt; could bond two connections. It can't: Drive accepts only one ordered byte stream per file, so &lt;code&gt;--via&lt;/code&gt; chooses a link rather than aggregating them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;drive.file&lt;/code&gt; scope is narrower than you'd think.&lt;/strong&gt; With the default &lt;code&gt;drive.file&lt;/code&gt; scope, gdup can only see folders it created or that were shared with the app. An existing folder it didn't create looks empty, so gdup may create a duplicate instead of reusing it. That's the trade-off for not asking for full Drive access.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bring your own OAuth client.&lt;/strong&gt; gdup uses your own Desktop-app OAuth client, so your uploads count against your quota and nobody else's app sits between you and your files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Grab &lt;code&gt;gdup.exe&lt;/code&gt; from the releases page and verify it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh attestation verify gdup.exe &lt;span class="nt"&gt;--repo&lt;/span&gt; shawnklewis/GoogleDriveResumableUploader
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exe isn't code-signed yet, so SmartScreen may warn on first run (More info → Run anyway).&lt;/p&gt;




&lt;p&gt;I'm a C#/.NET engineer in Trinidad &amp;amp; Tobago. Most of my work is Blazor, EF Core and SQL Server, plus embedded C# for motion control on a custom 5-axis 3D printer. If you're looking for someone who builds this kind of thing, I'm available for contract work.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>cli</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
