Skip to content

Instantly share code, notes, and snippets.

@yasirkula
Last active April 21, 2025 13:34
Show Gist options
  • Save yasirkula/d0ec0c07b138748e5feaecbd93b6223c to your computer and use it in GitHub Desktop.
Save yasirkula/d0ec0c07b138748e5feaecbd93b6223c to your computer and use it in GitHub Desktop.
C# Download Public File From Google Drive™ (works for large files as well)
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Net;
using System.Text;
/* EXAMPLE USAGE
FileDownloader fileDownloader = new FileDownloader();
// This callback is triggered for DownloadFileAsync only
fileDownloader.DownloadProgressChanged += ( sender, e ) => Console.WriteLine( "Progress changed " + e.BytesReceived + " " + e.TotalBytesToReceive );
// This callback is triggered for both DownloadFile and DownloadFileAsync
fileDownloader.DownloadFileCompleted += ( sender, e ) =>
{
if( e.Cancelled )
Console.WriteLine( "Download cancelled" );
else if( e.Error != null )
Console.WriteLine( "Download failed: " + e.Error );
else
Console.WriteLine( "Download completed" );
};
fileDownloader.DownloadFileAsync( "https://INSERT_DOWNLOAD_LINK_HERE", @"C:\downloadedFile.txt" );
*/
public class FileDownloader : IDisposable
{
private const string GOOGLE_DRIVE_DOMAIN = "drive.google.com";
private const string GOOGLE_DRIVE_DOMAIN2 = "https://drive.google.com";
// In the worst case, it is necessary to send 3 download requests to the Drive address
// 1. an NID cookie is returned instead of a download_warning cookie
// 2. download_warning cookie returned
// 3. the actual file is downloaded
private const int GOOGLE_DRIVE_MAX_DOWNLOAD_ATTEMPT = 3;
public delegate void DownloadProgressChangedEventHandler( object sender, DownloadProgress progress );
// Custom download progress reporting (needed for Google Drive)
public class DownloadProgress
{
public long BytesReceived, TotalBytesToReceive;
public object UserState;
public int ProgressPercentage
{
get
{
if( TotalBytesToReceive > 0L )
return (int) ( ( (double) BytesReceived / TotalBytesToReceive ) * 100 );
return 0;
}
}
}
// Web client that preserves cookies (needed for Google Drive)
private class CookieAwareWebClient : WebClient
{
private class CookieContainer
{
private readonly Dictionary<string, string> cookies = new Dictionary<string, string>();
public string this[Uri address]
{
get
{
string cookie;
if( cookies.TryGetValue( address.Host, out cookie ) )
return cookie;
return null;
}
set
{
cookies[address.Host] = value;
}
}
}
private readonly CookieContainer cookies = new CookieContainer();
public DownloadProgress ContentRangeTarget;
protected override WebRequest GetWebRequest( Uri address )
{
WebRequest request = base.GetWebRequest( address );
if( request is HttpWebRequest )
{
string cookie = cookies[address];
if( cookie != null )
( (HttpWebRequest) request ).Headers.Set( "cookie", cookie );
if( ContentRangeTarget != null )
( (HttpWebRequest) request ).AddRange( 0 );
}
return request;
}
protected override WebResponse GetWebResponse( WebRequest request, IAsyncResult result )
{
return ProcessResponse( base.GetWebResponse( request, result ) );
}
protected override WebResponse GetWebResponse( WebRequest request )
{
return ProcessResponse( base.GetWebResponse( request ) );
}
private WebResponse ProcessResponse( WebResponse response )
{
string[] cookies = response.Headers.GetValues( "Set-Cookie" );
if( cookies != null && cookies.Length > 0 )
{
int length = 0;
for( int i = 0; i < cookies.Length; i++ )
length += cookies[i].Length;
StringBuilder cookie = new StringBuilder( length );
for( int i = 0; i < cookies.Length; i++ )
cookie.Append( cookies[i] );
this.cookies[response.ResponseUri] = cookie.ToString();
}
if( ContentRangeTarget != null )
{
string[] rangeLengthHeader = response.Headers.GetValues( "Content-Range" );
if( rangeLengthHeader != null && rangeLengthHeader.Length > 0 )
{
int splitIndex = rangeLengthHeader[0].LastIndexOf( '/' );
if( splitIndex >= 0 && splitIndex < rangeLengthHeader[0].Length - 1 )
{
long length;
if( long.TryParse( rangeLengthHeader[0].Substring( splitIndex + 1 ), out length ) )
ContentRangeTarget.TotalBytesToReceive = length;
}
}
}
return response;
}
}
private readonly CookieAwareWebClient webClient;
private readonly DownloadProgress downloadProgress;
private Uri downloadAddress;
private string downloadPath;
private bool asyncDownload;
private object userToken;
private bool downloadingDriveFile;
private int driveDownloadAttempt;
public event DownloadProgressChangedEventHandler DownloadProgressChanged;
public event AsyncCompletedEventHandler DownloadFileCompleted;
public FileDownloader()
{
webClient = new CookieAwareWebClient();
webClient.DownloadProgressChanged += DownloadProgressChangedCallback;
webClient.DownloadFileCompleted += DownloadFileCompletedCallback;
downloadProgress = new DownloadProgress();
}
public void DownloadFile( string address, string fileName )
{
DownloadFile( address, fileName, false, null );
}
public void DownloadFileAsync( string address, string fileName, object userToken = null )
{
DownloadFile( address, fileName, true, userToken );
}
private void DownloadFile( string address, string fileName, bool asyncDownload, object userToken )
{
downloadingDriveFile = address.StartsWith( GOOGLE_DRIVE_DOMAIN ) || address.StartsWith( GOOGLE_DRIVE_DOMAIN2 );
if( downloadingDriveFile )
{
address = GetGoogleDriveDownloadAddress( address );
driveDownloadAttempt = 1;
webClient.ContentRangeTarget = downloadProgress;
}
else
webClient.ContentRangeTarget = null;
downloadAddress = new Uri( address );
downloadPath = fileName;
downloadProgress.TotalBytesToReceive = -1L;
downloadProgress.UserState = userToken;
this.asyncDownload = asyncDownload;
this.userToken = userToken;
DownloadFileInternal();
}
private void DownloadFileInternal()
{
if( !asyncDownload )
{
webClient.DownloadFile( downloadAddress, downloadPath );
// This callback isn't triggered for synchronous downloads, manually trigger it
DownloadFileCompletedCallback( webClient, new AsyncCompletedEventArgs( null, false, null ) );
}
else if( userToken == null )
webClient.DownloadFileAsync( downloadAddress, downloadPath );
else
webClient.DownloadFileAsync( downloadAddress, downloadPath, userToken );
}
private void DownloadProgressChangedCallback( object sender, DownloadProgressChangedEventArgs e )
{
if( DownloadProgressChanged != null )
{
downloadProgress.BytesReceived = e.BytesReceived;
if( e.TotalBytesToReceive > 0L )
downloadProgress.TotalBytesToReceive = e.TotalBytesToReceive;
DownloadProgressChanged( this, downloadProgress );
}
}
private void DownloadFileCompletedCallback( object sender, AsyncCompletedEventArgs e )
{
if( !downloadingDriveFile )
{
if( DownloadFileCompleted != null )
DownloadFileCompleted( this, e );
}
else
{
if( driveDownloadAttempt < GOOGLE_DRIVE_MAX_DOWNLOAD_ATTEMPT && !ProcessDriveDownload() )
{
// Try downloading the Drive file again
driveDownloadAttempt++;
DownloadFileInternal();
}
else if( DownloadFileCompleted != null )
DownloadFileCompleted( this, e );
}
}
// Downloading large files from Google Drive prompts a warning screen and requires manual confirmation
// Consider that case and try to confirm the download automatically if warning prompt occurs
// Returns true, if no more download requests are necessary
private bool ProcessDriveDownload()
{
FileInfo downloadedFile = new FileInfo( downloadPath );
if( downloadedFile == null )
return true;
// Confirmation page is around 50KB, shouldn't be larger than 60KB
if( downloadedFile.Length > 60000L )
return true;
// Downloaded file might be the confirmation page, check it
string content;
using( var reader = downloadedFile.OpenText() )
{
// Confirmation page starts with <!DOCTYPE html>, which can be preceeded by a newline
char[] header = new char[20];
int readCount = reader.ReadBlock( header, 0, 20 );
if( readCount < 20 || !( new string( header ).Contains( "<!DOCTYPE html>" ) ) )
return true;
content = reader.ReadToEnd();
}
int linkIndex = content.LastIndexOf( "href=\"/uc?" );
if( linkIndex >= 0 )
{
linkIndex += 6;
int linkEnd = content.IndexOf( '"', linkIndex );
if( linkEnd >= 0 )
{
downloadAddress = new Uri( "https://drive.google.com" + content.Substring( linkIndex, linkEnd - linkIndex ).Replace( "&amp;", "&" ) );
return false;
}
}
int formIndex = content.LastIndexOf( "<form id=\"download-form\"" );
if( formIndex >= 0 )
{
int formEndIndex = content.IndexOf( "</form>", formIndex + 10 );
int inputIndex = formIndex;
StringBuilder sb = new StringBuilder().Append( "https://drive.usercontent.google.com/download" );
bool isFirstArgument = true;
while( ( inputIndex = content.IndexOf( "<input type=\"hidden\"", inputIndex + 10 ) ) >= 0 && inputIndex < formEndIndex )
{
linkIndex = content.IndexOf( "name=", inputIndex + 10 ) + 6;
sb.Append( isFirstArgument ? '?' : '&' ).Append( content, linkIndex, content.IndexOf( '"', linkIndex ) - linkIndex ).Append( '=' );
linkIndex = content.IndexOf( "value=", linkIndex ) + 7;
sb.Append( content, linkIndex, content.IndexOf( '"', linkIndex ) - linkIndex );
isFirstArgument = false;
}
downloadAddress = new Uri( sb.ToString() );
return false;
}
return true;
}
// Handles the following formats (links can be preceeded by https://):
// - drive.google.com/open?id=FILEID&resourcekey=RESOURCEKEY
// - drive.google.com/file/d/FILEID/view?usp=sharing&resourcekey=RESOURCEKEY
// - drive.google.com/uc?id=FILEID&export=download&resourcekey=RESOURCEKEY
private string GetGoogleDriveDownloadAddress( string address )
{
int index = address.IndexOf( "id=" );
int closingIndex;
if( index > 0 )
{
index += 3;
closingIndex = address.IndexOf( '&', index );
if( closingIndex < 0 )
closingIndex = address.Length;
}
else
{
index = address.IndexOf( "file/d/" );
if( index < 0 ) // address is not in any of the supported forms
return string.Empty;
index += 7;
closingIndex = address.IndexOf( '/', index );
if( closingIndex < 0 )
{
closingIndex = address.IndexOf( '?', index );
if( closingIndex < 0 )
closingIndex = address.Length;
}
}
string fileID = address.Substring( index, closingIndex - index );
index = address.IndexOf( "resourcekey=" );
if( index > 0 )
{
index += 12;
closingIndex = address.IndexOf( '&', index );
if( closingIndex < 0 )
closingIndex = address.Length;
string resourceKey = address.Substring( index, closingIndex - index );
return string.Concat( "https://drive.google.com/uc?id=", fileID, "&export=download&resourcekey=", resourceKey, "&confirm=t" );
}
else
return string.Concat( "https://drive.google.com/uc?id=", fileID, "&export=download&confirm=t" );
}
public void Dispose()
{
webClient.Dispose();
}
}
@yasirkula
Copy link
Author

@BECCAKTN I was able to reproduce the issue with a 300 MB download. It downloaded only 50 MB for both async and sync solutions. But, I've tried downloading the file from Drive using my web browser and surprisingly, my browser was also able to download only 50 MB. So I guess it's a bug on Drive-side. Can you try downloading using your browser? I've tested on Chrome.

@BECCAKTN
Copy link

BECCAKTN commented Jan 4, 2025

Hey yasirkula is it easy to implement a new feature to read a simple text file on gdrive without downloading it like httpwebrequest.create to .Get response and read it with streamreader?
I have a winform that uses a txt file and reads it for a version number and if an update is needed it goes ahead and uses what you already made to download the files

Thx!

@yasirkula
Copy link
Author

@BECCAKTN Hi! Our web browsers can preview text files but I don't know how to accomplish it in code.

@BECCAKTN
Copy link

BECCAKTN commented Jan 6, 2025

Yasirkula I want to thank you for your code here.
It has enabled me to work with Google drive from code.
Actually I now do not directly use your code at run time but I use it as a tool to get the real download able url because as the dowloadfileinternal method is done with things, like the 3rd time, I get the url that I need at run time which I just put that value in my app.config.

@yasirkula
Copy link
Author

That download link can expire after a short time or not work at all due to missing cookies. I'd recommend attempting to download a large file (> 50MB) from one such url after 10 minutes.

@BECCAKTN
Copy link

BECCAKTN commented Jan 6, 2025

@BECCAKTN Hi! Our web browsers can preview text files but I don't know how to accomplish it in code.

I found its quite easy with webclient.downloadstring

@BECCAKTN
Copy link

BECCAKTN commented Jan 10, 2025

That download link can expire after a short time or not work at all due to missing cookies. I'd recommend attempting to download a large file (> 50MB) from one such url after 10 minutes.

It's been a week so far and still works with a 500mb file lol

@yasirkula
Copy link
Author

@BECCAKTN Glad to hear it :)

@Aergernis
Copy link

Hi, great tool to download from gDrive.

Is it possible to download multiple files async and wait for all to finish before proceeding with the execution of a ButtonClick event with WPF?

Click Button -> download file 1 -> download file 2 -> ... -> proceed with the rest of the click action?

I'm just learning C# and can't figure out a good way.

@yasirkula
Copy link
Author

yasirkula commented Apr 12, 2025

@Aergernis In my opinion the simplest way to work with async operations is to use C# Tasks. You can use TaskCompletionSource to convert FileDownloader's async job to an actual Task. Then you can use all sorts of helper functions like Task.WaitAll.

@Aergernis
Copy link

Thanks for the fast response and the hint with TaskCompletionSource.
Learned something new :)

The solution i came up with is

        public Task DownloadTask(string address, string fileName)
        {
            var tcs = new TaskCompletionSource<object>();
            FileDownloader fileDownloader = new FileDownloader();
            stopwatch.Start();
            fileDownloader.DownloadFileAsync(address, fileName);
            fileDownloader.DownloadProgressChanged += (sender, e) =>
            {
                string downloadProgress = e.ProgressPercentage + "%";
                string downloadSpeed = string.Format("{0} MB/s", (e.BytesReceived / 1024.0 / 1024.0 / stopwatch.Elapsed.TotalSeconds).ToString("0.00"));
                string downloadedMBs = Math.Round(e.BytesReceived / 1024.0 / 1024.0) + " MB";
                string totalMBs = Math.Round(e.TotalBytesToReceive / 1024.0 / 1024.0) + " MB";
                string progress = $"{fileName.Split("\\").Last()}: {downloadedMBs} / {totalMBs} ({downloadProgress}) @ {downloadSpeed}";

                progressBar1.Value = e.ProgressPercentage;
                progressBar1.Update();

                labelProgress.Invoke((MethodInvoker)(() => labelProgress.Text = progress));

            };
            fileDownloader.DownloadFileCompleted += (sender, e) =>
            {
                if (e.Cancelled)
                {
                    stopwatch.Reset();
                    buttonDownload.Enabled = true;
                    tcs.SetResult(null);
                    MessageBox.Show("Download cancelled");
                }
                else if (e.Error != null)
                {
                    stopwatch.Reset();
                    buttonDownload.Enabled = true;
                    tcs.SetResult(null);
                    MessageBox.Show("Download failed: " + e.Error);
                }
                else
                {
                    stopwatch.Reset();
                    buttonDownload.Enabled = true;
                    tcs.SetResult(null);
                }
            };

            return tcs.Task;
        }

		private async void button1_Click(object sender, EventArgs e)
        {
            await DownloadTask("https://drive.google.com/file/d/xxxx/view?usp=sharing", fil.zip));
        }

Any suggestions if i can make something better?

@yasirkula
Copy link
Author

@Aergernis Your code looks acceptable to me, well done.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment